diff options
Diffstat (limited to 'cmds/uiautomator/library/core-src')
14 files changed, 6132 insertions, 0 deletions
diff --git a/cmds/uiautomator/library/core-src/com/android/uiautomator/core/AccessibilityNodeInfoDumper.java b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/AccessibilityNodeInfoDumper.java new file mode 100644 index 0000000..63c51e8 --- /dev/null +++ b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/AccessibilityNodeInfoDumper.java @@ -0,0 +1,250 @@ +/* + * 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.os.Environment; +import android.os.SystemClock; +import android.util.Log; +import android.util.Xml; +import android.view.accessibility.AccessibilityNodeInfo; + +import org.xmlpull.v1.XmlSerializer; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.StringWriter; + +/** + * + * @hide + */ +public class AccessibilityNodeInfoDumper { + + private static final String LOGTAG = AccessibilityNodeInfoDumper.class.getSimpleName(); + private static final String[] NAF_EXCLUDED_CLASSES = new String[] { + android.widget.GridView.class.getName(), android.widget.GridLayout.class.getName(), + android.widget.ListView.class.getName(), android.widget.TableLayout.class.getName() + }; + + /** + * Using {@link AccessibilityNodeInfo} this method will walk the layout hierarchy + * and generates an xml dump into the /data/local/window_dump.xml + * @param root The root accessibility node. + * @param rotation The rotaion of current display + * @param width The pixel width of current display + * @param height The pixel height of current display + */ + public static void dumpWindowToFile(AccessibilityNodeInfo root, int rotation, + int width, int height) { + File baseDir = new File(Environment.getDataDirectory(), "local"); + if (!baseDir.exists()) { + baseDir.mkdir(); + baseDir.setExecutable(true, false); + baseDir.setWritable(true, false); + baseDir.setReadable(true, false); + } + dumpWindowToFile(root, + new File(new File(Environment.getDataDirectory(), "local"), "window_dump.xml"), + rotation, width, height); + } + + /** + * Using {@link AccessibilityNodeInfo} this method will walk the layout hierarchy + * and generates an xml dump to the location specified by <code>dumpFile</code> + * @param root The root accessibility node. + * @param dumpFile The file to dump to. + * @param rotation The rotaion of current display + * @param width The pixel width of current display + * @param height The pixel height of current display + */ + public static void dumpWindowToFile(AccessibilityNodeInfo root, File dumpFile, int rotation, + int width, int height) { + if (root == null) { + return; + } + final long startTime = SystemClock.uptimeMillis(); + try { + FileWriter writer = new FileWriter(dumpFile); + XmlSerializer serializer = Xml.newSerializer(); + StringWriter stringWriter = new StringWriter(); + serializer.setOutput(stringWriter); + serializer.startDocument("UTF-8", true); + serializer.startTag("", "hierarchy"); + serializer.attribute("", "rotation", Integer.toString(rotation)); + dumpNodeRec(root, serializer, 0, width, height); + serializer.endTag("", "hierarchy"); + serializer.endDocument(); + writer.write(stringWriter.toString()); + writer.close(); + } catch (IOException e) { + Log.e(LOGTAG, "failed to dump window to file", e); + } + final long endTime = SystemClock.uptimeMillis(); + Log.w(LOGTAG, "Fetch time: " + (endTime - startTime) + "ms"); + } + + private static void dumpNodeRec(AccessibilityNodeInfo node, XmlSerializer serializer,int index, + int width, int height) throws IOException { + serializer.startTag("", "node"); + if (!nafExcludedClass(node) && !nafCheck(node)) + serializer.attribute("", "NAF", Boolean.toString(true)); + serializer.attribute("", "index", Integer.toString(index)); + serializer.attribute("", "text", safeCharSeqToString(node.getText())); + 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())); + serializer.attribute("", "checkable", Boolean.toString(node.isCheckable())); + serializer.attribute("", "checked", Boolean.toString(node.isChecked())); + serializer.attribute("", "clickable", Boolean.toString(node.isClickable())); + serializer.attribute("", "enabled", Boolean.toString(node.isEnabled())); + serializer.attribute("", "focusable", Boolean.toString(node.isFocusable())); + serializer.attribute("", "focused", Boolean.toString(node.isFocused())); + serializer.attribute("", "scrollable", Boolean.toString(node.isScrollable())); + serializer.attribute("", "long-clickable", Boolean.toString(node.isLongClickable())); + serializer.attribute("", "password", Boolean.toString(node.isPassword())); + serializer.attribute("", "selected", Boolean.toString(node.isSelected())); + serializer.attribute("", "bounds", AccessibilityNodeInfoHelper.getVisibleBoundsInScreen( + node, width, height).toShortString()); + int count = node.getChildCount(); + for (int i = 0; i < count; i++) { + AccessibilityNodeInfo child = node.getChild(i); + if (child != null) { + if (child.isVisibleToUser()) { + dumpNodeRec(child, serializer, i, width, height); + child.recycle(); + } else { + Log.i(LOGTAG, String.format("Skipping invisible child: %s", child.toString())); + } + } else { + Log.i(LOGTAG, String.format("Null child %d/%d, parent: %s", + i, count, node.toString())); + } + } + serializer.endTag("", "node"); + } + + /** + * The list of classes to exclude my not be complete. We're attempting to + * only reduce noise from standard layout classes that may be falsely + * configured to accept clicks and are also enabled. + * + * @param node + * @return true if node is excluded. + */ + private static boolean nafExcludedClass(AccessibilityNodeInfo node) { + String className = safeCharSeqToString(node.getClassName()); + for(String excludedClassName : NAF_EXCLUDED_CLASSES) { + if(className.endsWith(excludedClassName)) + return true; + } + return false; + } + + /** + * We're looking for UI controls that are enabled, clickable but have no + * text nor content-description. Such controls configuration indicate an + * interactive control is present in the UI and is most likely not + * accessibility friendly. We refer to such controls here as NAF controls + * (Not Accessibility Friendly) + * + * @param node + * @return false if a node fails the check, true if all is OK + */ + private static boolean nafCheck(AccessibilityNodeInfo node) { + boolean isNaf = node.isClickable() && node.isEnabled() + && safeCharSeqToString(node.getContentDescription()).isEmpty() + && safeCharSeqToString(node.getText()).isEmpty(); + + if (!isNaf) + return true; + + // check children since sometimes the containing element is clickable + // and NAF but a child's text or description is available. Will assume + // such layout as fine. + return childNafCheck(node); + } + + /** + * This should be used when it's already determined that the node is NAF and + * a further check of its children is in order. A node maybe a container + * such as LinerLayout and may be set to be clickable but have no text or + * content description but it is counting on one of its children to fulfill + * the requirement for being accessibility friendly by having one or more of + * its children fill the text or content-description. Such a combination is + * considered by this dumper as acceptable for accessibility. + * + * @param node + * @return false if node fails the check. + */ + private static boolean childNafCheck(AccessibilityNodeInfo node) { + int childCount = node.getChildCount(); + for (int x = 0; x < childCount; x++) { + AccessibilityNodeInfo childNode = node.getChild(x); + + if (!safeCharSeqToString(childNode.getContentDescription()).isEmpty() + || !safeCharSeqToString(childNode.getText()).isEmpty()) + return true; + + if (childNafCheck(childNode)) + return true; + } + return false; + } + + private static String safeCharSeqToString(CharSequence cs) { + if (cs == null) + return ""; + else { + return stripInvalidXMLChars(cs); + } + } + + private static String stripInvalidXMLChars(CharSequence cs) { + StringBuffer ret = new StringBuffer(); + char ch; + /* http://www.w3.org/TR/xml11/#charsets + [#x1-#x8], [#xB-#xC], [#xE-#x1F], [#x7F-#x84], [#x86-#x9F], [#xFDD0-#xFDDF], + [#x1FFFE-#x1FFFF], [#x2FFFE-#x2FFFF], [#x3FFFE-#x3FFFF], + [#x4FFFE-#x4FFFF], [#x5FFFE-#x5FFFF], [#x6FFFE-#x6FFFF], + [#x7FFFE-#x7FFFF], [#x8FFFE-#x8FFFF], [#x9FFFE-#x9FFFF], + [#xAFFFE-#xAFFFF], [#xBFFFE-#xBFFFF], [#xCFFFE-#xCFFFF], + [#xDFFFE-#xDFFFF], [#xEFFFE-#xEFFFF], [#xFFFFE-#xFFFFF], + [#x10FFFE-#x10FFFF]. + */ + for (int i = 0; i < cs.length(); i++) { + ch = cs.charAt(i); + + if((ch >= 0x1 && ch <= 0x8) || (ch >= 0xB && ch <= 0xC) || (ch >= 0xE && ch <= 0x1F) || + (ch >= 0x7F && ch <= 0x84) || (ch >= 0x86 && ch <= 0x9f) || + (ch >= 0xFDD0 && ch <= 0xFDDF) || (ch >= 0x1FFFE && ch <= 0x1FFFF) || + (ch >= 0x2FFFE && ch <= 0x2FFFF) || (ch >= 0x3FFFE && ch <= 0x3FFFF) || + (ch >= 0x4FFFE && ch <= 0x4FFFF) || (ch >= 0x5FFFE && ch <= 0x5FFFF) || + (ch >= 0x6FFFE && ch <= 0x6FFFF) || (ch >= 0x7FFFE && ch <= 0x7FFFF) || + (ch >= 0x8FFFE && ch <= 0x8FFFF) || (ch >= 0x9FFFE && ch <= 0x9FFFF) || + (ch >= 0xAFFFE && ch <= 0xAFFFF) || (ch >= 0xBFFFE && ch <= 0xBFFFF) || + (ch >= 0xCFFFE && ch <= 0xCFFFF) || (ch >= 0xDFFFE && ch <= 0xDFFFF) || + (ch >= 0xEFFFE && ch <= 0xEFFFF) || (ch >= 0xFFFFE && ch <= 0xFFFFF) || + (ch >= 0x10FFFE && ch <= 0x10FFFF)) + ret.append("."); + else + ret.append(ch); + } + return ret.toString(); + } +} diff --git a/cmds/uiautomator/library/core-src/com/android/uiautomator/core/AccessibilityNodeInfoHelper.java b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/AccessibilityNodeInfoHelper.java new file mode 100644 index 0000000..54835e3 --- /dev/null +++ b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/AccessibilityNodeInfoHelper.java @@ -0,0 +1,52 @@ +/* + * 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.graphics.Rect; +import android.view.accessibility.AccessibilityNodeInfo; + +/** + * This class contains static helper methods to work with + * {@link AccessibilityNodeInfo} + */ +class AccessibilityNodeInfoHelper { + + /** + * Returns the node's bounds clipped to the size of the display + * + * @param node + * @param width pixel width of the display + * @param height pixel height of the display + * @return null if node is null, else a Rect containing visible bounds + */ + static Rect getVisibleBoundsInScreen(AccessibilityNodeInfo node, int width, int height) { + if (node == null) { + return null; + } + // targeted node's bounds + Rect nodeRect = new Rect(); + node.getBoundsInScreen(nodeRect); + + Rect displayRect = new Rect(); + displayRect.top = 0; + displayRect.left = 0; + displayRect.right = width; + displayRect.bottom = height; + + nodeRect.intersect(displayRect); + return nodeRect; + } +} diff --git a/cmds/uiautomator/library/core-src/com/android/uiautomator/core/Configurator.java b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/Configurator.java new file mode 100644 index 0000000..249f404 --- /dev/null +++ b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/Configurator.java @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2013 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; + +/** + * Allows you to set key parameters for running uiautomator tests. The new + * settings take effect immediately and can be changed any time during a test run. + * + * To modify parameters using Configurator, first obtain an instance by calling + * {@link #getInstance()}. As a best practice, make sure you always save + * the original value of any parameter that you are modifying. After running your + * tests with the modified parameters, make sure to also restore + * the original parameter values, otherwise this will impact other tests cases. + * @since API Level 18 + */ +public final class Configurator { + private long mWaitForIdleTimeout = 10 * 1000; + private long mWaitForSelector = 10 * 1000; + private long mWaitForActionAcknowledgment = 3 * 1000; + + // 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 long mScrollEventWaitTimeout = 200; // ms + + // Default is inject as fast as we can + private long mKeyInjectionDelay = 0; // ms + + // reference to self + private static Configurator sConfigurator; + + private Configurator() { + /* hide constructor */ + } + + /** + * Retrieves a singleton instance of Configurator. + * + * @return Configurator instance + * @since API Level 18 + */ + public static Configurator getInstance() { + if (sConfigurator == null) { + sConfigurator = new Configurator(); + } + return sConfigurator; + } + + /** + * Sets the timeout for waiting for the user interface to go into an idle + * state before starting a uiautomator action. + * + * By default, all core uiautomator objects except {@link UiDevice} will perform + * this wait before starting to search for the widget specified by the + * object's {@link UiSelector}. Once the idle state is detected or the + * timeout elapses (whichever occurs first), the object will start to wait + * for the selector to find a match. + * See {@link #setWaitForSelectorTimeout(long)} + * + * @param timeout Timeout value in milliseconds + * @return self + * @since API Level 18 + */ + public Configurator setWaitForIdleTimeout(long timeout) { + mWaitForIdleTimeout = timeout; + return this; + } + + /** + * Gets the current timeout used for waiting for the user interface to go + * into an idle state. + * + * By default, all core uiautomator objects except {@link UiDevice} will perform + * this wait before starting to search for the widget specified by the + * object's {@link UiSelector}. Once the idle state is detected or the + * timeout elapses (whichever occurs first), the object will start to wait + * for the selector to find a match. + * See {@link #setWaitForSelectorTimeout(long)} + * + * @return Current timeout value in milliseconds + * @since API Level 18 + */ + public long getWaitForIdleTimeout() { + return mWaitForIdleTimeout; + } + + /** + * Sets the timeout for waiting for a widget to become visible in the user + * interface so that it can be matched by a selector. + * + * Because user interface content is dynamic, sometimes a widget may not + * be visible immediately and won't be detected by a selector. This timeout + * allows the uiautomator framework to wait for a match to be found, up until + * the timeout elapses. + * + * @param timeout Timeout value in milliseconds. + * @return self + * @since API Level 18 + */ + public Configurator setWaitForSelectorTimeout(long timeout) { + mWaitForSelector = timeout; + return this; + } + + /** + * Gets the current timeout for waiting for a widget to become visible in + * the user interface so that it can be matched by a selector. + * + * Because user interface content is dynamic, sometimes a widget may not + * be visible immediately and won't be detected by a selector. This timeout + * allows the uiautomator framework to wait for a match to be found, up until + * the timeout elapses. + * + * @return Current timeout value in milliseconds + * @since API Level 18 + */ + public long getWaitForSelectorTimeout() { + return mWaitForSelector; + } + + /** + * Sets the timeout for waiting for an acknowledgement of an + * uiautomtor scroll swipe action. + * + * The acknowledgment is an <a href="http://developer.android.com/reference/android/view/accessibility/AccessibilityEvent.html">AccessibilityEvent</a>, + * corresponding to the scroll action, that lets the framework determine if + * the scroll action was successful. Generally, this timeout should not be modified. + * See {@link UiScrollable} + * + * @param timeout Timeout value in milliseconds + * @return self + * @since API Level 18 + */ + public Configurator setScrollAcknowledgmentTimeout(long timeout) { + mScrollEventWaitTimeout = timeout; + return this; + } + + /** + * Gets the timeout for waiting for an acknowledgement of an + * uiautomtor scroll swipe action. + * + * The acknowledgment is an <a href="http://developer.android.com/reference/android/view/accessibility/AccessibilityEvent.html">AccessibilityEvent</a>, + * corresponding to the scroll action, that lets the framework determine if + * the scroll action was successful. Generally, this timeout should not be modified. + * See {@link UiScrollable} + * + * @return current timeout in milliseconds + * @since API Level 18 + */ + public long getScrollAcknowledgmentTimeout() { + return mScrollEventWaitTimeout; + } + + /** + * Sets the timeout for waiting for an acknowledgment of generic uiautomator + * actions, such as clicks, text setting, and menu presses. + * + * The acknowledgment is an <a href="http://developer.android.com/reference/android/view/accessibility/AccessibilityEvent.html">AccessibilityEvent</a>, + * corresponding to an action, that lets the framework determine if the + * action was successful. Generally, this timeout should not be modified. + * See {@link UiObject} + * + * @param timeout Timeout value in milliseconds + * @return self + * @since API Level 18 + */ + public Configurator setActionAcknowledgmentTimeout(long timeout) { + mWaitForActionAcknowledgment = timeout; + return this; + } + + /** + * Gets the current timeout for waiting for an acknowledgment of generic + * uiautomator actions, such as clicks, text setting, and menu presses. + * + * The acknowledgment is an <a href="http://developer.android.com/reference/android/view/accessibility/AccessibilityEvent.html">AccessibilityEvent</a>, + * corresponding to an action, that lets the framework determine if the + * action was successful. Generally, this timeout should not be modified. + * See {@link UiObject} + * + * @return current timeout in milliseconds + * @since API Level 18 + */ + public long getActionAcknowledgmentTimeout() { + return mWaitForActionAcknowledgment; + } + + /** + * Sets a delay between key presses when injecting text input. + * See {@link UiObject#setText(String)} + * + * @param delay Delay value in milliseconds + * @return self + * @since API Level 18 + */ + public Configurator setKeyInjectionDelay(long delay) { + mKeyInjectionDelay = delay; + return this; + } + + /** + * Gets the current delay between key presses when injecting text input. + * See {@link UiObject#setText(String)} + * + * @return current delay in milliseconds + * @since API Level 18 + */ + public long getKeyInjectionDelay() { + return mKeyInjectionDelay; + } +} diff --git a/cmds/uiautomator/library/core-src/com/android/uiautomator/core/InteractionController.java b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/InteractionController.java new file mode 100644 index 0000000..73e46f1 --- /dev/null +++ b/cmds/uiautomator/library/core-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); + } +} diff --git a/cmds/uiautomator/library/core-src/com/android/uiautomator/core/QueryController.java b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/QueryController.java new file mode 100644 index 0000000..6931528 --- /dev/null +++ b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/QueryController.java @@ -0,0 +1,521 @@ +/* + * 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.app.UiAutomation.OnAccessibilityEventListener; +import android.os.SystemClock; +import android.util.Log; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; + + +/** + * The QueryController main purpose is to translate a {@link UiSelector} selectors to + * {@link AccessibilityNodeInfo}. This is all this controller does. + */ +class QueryController { + + private static final String LOG_TAG = QueryController.class.getSimpleName(); + + private static final boolean DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG); + private static final boolean VERBOSE = Log.isLoggable(LOG_TAG, Log.VERBOSE); + + private final UiAutomatorBridge mUiAutomatorBridge; + + private final Object mLock = new Object(); + + private String mLastActivityName = null; + + // During a pattern selector search, the recursive pattern search + // methods will track their counts and indexes here. + private int mPatternCounter = 0; + private int mPatternIndexer = 0; + + // These help show each selector's search context as it relates to the previous sub selector + // matched. When a compound selector fails, it is hard to tell which part of it is failing. + // Seeing how a selector is being parsed and which sub selector failed within a long list + // of compound selectors is very helpful. + private int mLogIndent = 0; + private int mLogParentIndent = 0; + + private String mLastTraversedText = ""; + + public QueryController(UiAutomatorBridge bridge) { + mUiAutomatorBridge = bridge; + bridge.setOnAccessibilityEventListener(new OnAccessibilityEventListener() { + @Override + public void onAccessibilityEvent(AccessibilityEvent event) { + synchronized (mLock) { + switch(event.getEventType()) { + case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: + // don't trust event.getText(), check for nulls + if (event.getText() != null && event.getText().size() > 0) { + if(event.getText().get(0) != null) + mLastActivityName = event.getText().get(0).toString(); + } + break; + case AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY: + // don't trust event.getText(), check for nulls + if (event.getText() != null && event.getText().size() > 0) + if(event.getText().get(0) != null) + mLastTraversedText = event.getText().get(0).toString(); + if (DEBUG) + Log.d(LOG_TAG, "Last text selection reported: " + + mLastTraversedText); + break; + } + mLock.notifyAll(); + } + } + }); + } + + /** + * Returns the last text selection reported by accessibility + * event TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY. One way to cause + * this event is using a DPad arrows to focus on UI elements. + */ + public String getLastTraversedText() { + mUiAutomatorBridge.waitForIdle(); + synchronized (mLock) { + if (mLastTraversedText.length() > 0) { + return mLastTraversedText; + } + } + return null; + } + + /** + * Clears the last text selection value saved from the TYPE_VIEW_TEXT_SELECTION_CHANGED + * event + */ + public void clearLastTraversedText() { + mUiAutomatorBridge.waitForIdle(); + synchronized (mLock) { + mLastTraversedText = ""; + } + } + + private void initializeNewSearch() { + mPatternCounter = 0; + mPatternIndexer = 0; + mLogIndent = 0; + mLogParentIndent = 0; + } + + /** + * Counts the instances of the selector group. The selector must be in the following + * format: [container_selector, PATTERN=[INSTANCE=x, PATTERN=[the_pattern]] + * where the container_selector is used to find the containment region to search for patterns + * and the INSTANCE=x is the instance of the_pattern to return. + * @param selector + * @return number of pattern matches. Returns 0 for all other cases. + */ + public int getPatternCount(UiSelector selector) { + findAccessibilityNodeInfo(selector, true /*counting*/); + return mPatternCounter; + } + + /** + * Main search method for translating By selectors to AccessibilityInfoNodes + * @param selector + * @return AccessibilityNodeInfo + */ + public AccessibilityNodeInfo findAccessibilityNodeInfo(UiSelector selector) { + return findAccessibilityNodeInfo(selector, false); + } + + protected AccessibilityNodeInfo findAccessibilityNodeInfo(UiSelector selector, + boolean isCounting) { + mUiAutomatorBridge.waitForIdle(); + initializeNewSearch(); + + if (DEBUG) + Log.d(LOG_TAG, "Searching: " + selector); + + synchronized (mLock) { + AccessibilityNodeInfo rootNode = getRootNode(); + if (rootNode == null) { + Log.e(LOG_TAG, "Cannot proceed when root node is null. Aborted search"); + return null; + } + + // Copy so that we don't modify the original's sub selectors + UiSelector uiSelector = new UiSelector(selector); + return translateCompoundSelector(uiSelector, rootNode, isCounting); + } + } + + /** + * Gets the root node from accessibility and if it fails to get one it will + * retry every 250ms for up to 1000ms. + * @return null if no root node is obtained + */ + protected AccessibilityNodeInfo getRootNode() { + final int maxRetry = 4; + final long waitInterval = 250; + AccessibilityNodeInfo rootNode = null; + for(int x = 0; x < maxRetry; x++) { + rootNode = mUiAutomatorBridge.getRootInActiveWindow(); + if (rootNode != null) { + return rootNode; + } + if(x < maxRetry - 1) { + Log.e(LOG_TAG, "Got null root node from accessibility - Retrying..."); + SystemClock.sleep(waitInterval); + } + } + return rootNode; + } + + /** + * A compoundSelector encapsulate both Regular and Pattern selectors. The formats follows: + * <p/> + * regular_selector = By[attributes... CHILD=By[attributes... CHILD=By[....]]] + * <br/> + * pattern_selector = ...CONTAINER=By[..] PATTERN=By[instance=x PATTERN=[regular_selector] + * <br/> + * compound_selector = [regular_selector [pattern_selector]] + * <p/> + * regular_selectors are the most common form of selectors and the search for them + * is straightforward. On the other hand pattern_selectors requires search to be + * performed as in regular_selector but where regular_selector search returns immediately + * upon a successful match, the search for pattern_selector continues until the + * requested matched _instance_ of that pattern is matched. + * <p/> + * Counting UI objects requires using pattern_selectors. The counting search is the same + * as a pattern_search however we're not looking to match an instance of the pattern but + * rather continuously walking the accessibility node hierarchy while counting matched + * patterns, until the end of the tree. + * <p/> + * If both present, order of parsing begins with CONTAINER followed by PATTERN then the + * top most selector is processed as regular_selector within the context of the previous + * CONTAINER and its PATTERN information. If neither is present then the top selector is + * directly treated as regular_selector. So the presence of a CONTAINER and PATTERN within + * a selector simply dictates that the selector matching will be constraint to the sub tree + * node where the CONTAINER and its child PATTERN have identified. + * @param selector + * @param fromNode + * @param isCounting + * @return AccessibilityNodeInfo + */ + private AccessibilityNodeInfo translateCompoundSelector(UiSelector selector, + AccessibilityNodeInfo fromNode, boolean isCounting) { + + // Start translating compound selectors by translating the regular_selector first + // The regular_selector is then used as a container for any optional pattern_selectors + // that may or may not be specified. + if(selector.hasContainerSelector()) + // nested pattern selectors + if(selector.getContainerSelector().hasContainerSelector()) { + fromNode = translateCompoundSelector( + selector.getContainerSelector(), fromNode, false); + initializeNewSearch(); + } else + fromNode = translateReqularSelector(selector.getContainerSelector(), fromNode); + else + fromNode = translateReqularSelector(selector, fromNode); + + if(fromNode == null) { + if (DEBUG) + Log.d(LOG_TAG, "Container selector not found: " + selector.dumpToString(false)); + return null; + } + + if(selector.hasPatternSelector()) { + fromNode = translatePatternSelector(selector.getPatternSelector(), + fromNode, isCounting); + + if (isCounting) { + Log.i(LOG_TAG, String.format( + "Counted %d instances of: %s", mPatternCounter, selector)); + return null; + } else { + if(fromNode == null) { + if (DEBUG) + Log.d(LOG_TAG, "Pattern selector not found: " + + selector.dumpToString(false)); + return null; + } + } + } + + // translate any additions to the selector that may have been added by tests + // with getChild(By selector) after a container and pattern selectors + if(selector.hasContainerSelector() || selector.hasPatternSelector()) { + if(selector.hasChildSelector() || selector.hasParentSelector()) + fromNode = translateReqularSelector(selector, fromNode); + } + + if(fromNode == null) { + if (DEBUG) + Log.d(LOG_TAG, "Object Not Found for selector " + selector); + return null; + } + Log.i(LOG_TAG, String.format("Matched selector: %s <<==>> [%s]", selector, fromNode)); + return fromNode; + } + + /** + * Used by the {@link #translateCompoundSelector(UiSelector, AccessibilityNodeInfo, boolean)} + * to translate the regular_selector portion. It has the following format: + * <p/> + * regular_selector = By[attributes... CHILD=By[attributes... CHILD=By[....]]]<br/> + * <p/> + * regular_selectors are the most common form of selectors and the search for them + * is straightforward. This method will only look for CHILD or PARENT sub selectors. + * <p/> + * @param selector + * @param fromNode + * @return AccessibilityNodeInfo if found else null + */ + private AccessibilityNodeInfo translateReqularSelector(UiSelector selector, + AccessibilityNodeInfo fromNode) { + + return findNodeRegularRecursive(selector, fromNode, 0); + } + + private AccessibilityNodeInfo findNodeRegularRecursive(UiSelector subSelector, + AccessibilityNodeInfo fromNode, int index) { + + if (subSelector.isMatchFor(fromNode, index)) { + if (DEBUG) { + Log.d(LOG_TAG, formatLog(String.format("%s", + subSelector.dumpToString(false)))); + } + if(subSelector.isLeaf()) { + return fromNode; + } + if(subSelector.hasChildSelector()) { + mLogIndent++; // next selector + subSelector = subSelector.getChildSelector(); + if(subSelector == null) { + Log.e(LOG_TAG, "Error: A child selector without content"); + return null; // there is an implementation fault + } + } else if(subSelector.hasParentSelector()) { + mLogIndent++; // next selector + subSelector = subSelector.getParentSelector(); + if(subSelector == null) { + Log.e(LOG_TAG, "Error: A parent selector without content"); + return null; // there is an implementation fault + } + // the selector requested we start at this level from + // the parent node from the one we just matched + fromNode = fromNode.getParent(); + if(fromNode == null) + return null; + } + } + + int childCount = fromNode.getChildCount(); + boolean hasNullChild = false; + for (int i = 0; i < childCount; i++) { + AccessibilityNodeInfo childNode = fromNode.getChild(i); + if (childNode == null) { + Log.w(LOG_TAG, String.format( + "AccessibilityNodeInfo returned a null child (%d of %d)", i, childCount)); + if (!hasNullChild) { + Log.w(LOG_TAG, String.format("parent = %s", fromNode.toString())); + } + hasNullChild = true; + continue; + } + if (!childNode.isVisibleToUser()) { + if (VERBOSE) + Log.v(LOG_TAG, + String.format("Skipping invisible child: %s", childNode.toString())); + continue; + } + AccessibilityNodeInfo retNode = findNodeRegularRecursive(subSelector, childNode, i); + if (retNode != null) { + return retNode; + } + } + return null; + } + + /** + * Used by the {@link #translateCompoundSelector(UiSelector, AccessibilityNodeInfo, boolean)} + * to translate the pattern_selector portion. It has the following format: + * <p/> + * pattern_selector = ... PATTERN=By[instance=x PATTERN=[regular_selector]]<br/> + * <p/> + * pattern_selectors requires search to be performed as regular_selector but where + * regular_selector search returns immediately upon a successful match, the search for + * pattern_selector continues until the requested matched instance of that pattern is + * encountered. + * <p/> + * Counting UI objects requires using pattern_selectors. The counting search is the same + * as a pattern_search however we're not looking to match an instance of the pattern but + * rather continuously walking the accessibility node hierarchy while counting patterns + * until the end of the tree. + * @param subSelector + * @param fromNode + * @param isCounting + * @return null of node is not found or if counting mode is true. + * See {@link #translateCompoundSelector(UiSelector, AccessibilityNodeInfo, boolean)} + */ + private AccessibilityNodeInfo translatePatternSelector(UiSelector subSelector, + AccessibilityNodeInfo fromNode, boolean isCounting) { + + if(subSelector.hasPatternSelector()) { + // Since pattern_selectors are also the type of selectors used when counting, + // we check if this is a counting run or an indexing run + if(isCounting) + //since we're counting, we reset the indexer so to terminates the search when + // the end of tree is reached. The count will be in mPatternCount + mPatternIndexer = -1; + else + // terminates the search once we match the pattern's instance + mPatternIndexer = subSelector.getInstance(); + + // A pattern is wrapped in a PATTERN[instance=x PATTERN[the_pattern]] + subSelector = subSelector.getPatternSelector(); + if(subSelector == null) { + Log.e(LOG_TAG, "Pattern portion of the selector is null or not defined"); + return null; // there is an implementation fault + } + // save the current indent level as parent indent before pattern searches + // begin under the current tree position. + mLogParentIndent = ++mLogIndent; + return findNodePatternRecursive(subSelector, fromNode, 0, subSelector); + } + + Log.e(LOG_TAG, "Selector must have a pattern selector defined"); // implementation fault? + return null; + } + + private AccessibilityNodeInfo findNodePatternRecursive( + UiSelector subSelector, AccessibilityNodeInfo fromNode, int index, + UiSelector originalPattern) { + + if (subSelector.isMatchFor(fromNode, index)) { + if(subSelector.isLeaf()) { + if(mPatternIndexer == 0) { + if (DEBUG) + Log.d(LOG_TAG, formatLog( + String.format("%s", subSelector.dumpToString(false)))); + return fromNode; + } else { + if (DEBUG) + Log.d(LOG_TAG, formatLog( + String.format("%s", subSelector.dumpToString(false)))); + mPatternCounter++; //count the pattern matched + mPatternIndexer--; //decrement until zero for the instance requested + + // At a leaf selector within a group and still not instance matched + // then reset the selector to continue search from current position + // in the accessibility tree for the next pattern match up until the + // pattern index hits 0. + subSelector = originalPattern; + // starting over with next pattern search so reset to parent level + mLogIndent = mLogParentIndent; + } + } else { + if (DEBUG) + Log.d(LOG_TAG, formatLog( + String.format("%s", subSelector.dumpToString(false)))); + + if(subSelector.hasChildSelector()) { + mLogIndent++; // next selector + subSelector = subSelector.getChildSelector(); + if(subSelector == null) { + Log.e(LOG_TAG, "Error: A child selector without content"); + return null; + } + } else if(subSelector.hasParentSelector()) { + mLogIndent++; // next selector + subSelector = subSelector.getParentSelector(); + if(subSelector == null) { + Log.e(LOG_TAG, "Error: A parent selector without content"); + return null; + } + fromNode = fromNode.getParent(); + if(fromNode == null) + return null; + } + } + } + + int childCount = fromNode.getChildCount(); + boolean hasNullChild = false; + for (int i = 0; i < childCount; i++) { + AccessibilityNodeInfo childNode = fromNode.getChild(i); + if (childNode == null) { + Log.w(LOG_TAG, String.format( + "AccessibilityNodeInfo returned a null child (%d of %d)", i, childCount)); + if (!hasNullChild) { + Log.w(LOG_TAG, String.format("parent = %s", fromNode.toString())); + } + hasNullChild = true; + continue; + } + if (!childNode.isVisibleToUser()) { + if (DEBUG) + Log.d(LOG_TAG, + String.format("Skipping invisible child: %s", childNode.toString())); + continue; + } + AccessibilityNodeInfo retNode = findNodePatternRecursive( + subSelector, childNode, i, originalPattern); + if (retNode != null) { + return retNode; + } + } + return null; + } + + public AccessibilityNodeInfo getAccessibilityRootNode() { + return mUiAutomatorBridge.getRootInActiveWindow(); + } + + /** + * Last activity to report accessibility events. + * @deprecated The results returned should be considered unreliable + * @return String name of activity + */ + @Deprecated + public String getCurrentActivityName() { + mUiAutomatorBridge.waitForIdle(); + synchronized (mLock) { + return mLastActivityName; + } + } + + /** + * Last package to report accessibility events + * @return String name of package + */ + public String getCurrentPackageName() { + mUiAutomatorBridge.waitForIdle(); + AccessibilityNodeInfo rootNode = getRootNode(); + if (rootNode == null) + return null; + return rootNode.getPackageName() != null ? rootNode.getPackageName().toString() : null; + } + + private String formatLog(String str) { + StringBuilder l = new StringBuilder(); + for(int space = 0; space < mLogIndent; space++) + l.append(". . "); + if(mLogIndent > 0) + l.append(String.format(". . [%d]: %s", mPatternCounter, str)); + else + l.append(String.format(". . [%d]: %s", mPatternCounter, str)); + return l.toString(); + } +} diff --git a/cmds/uiautomator/library/core-src/com/android/uiautomator/core/Tracer.java b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/Tracer.java new file mode 100644 index 0000000..d574fc0 --- /dev/null +++ b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/Tracer.java @@ -0,0 +1,285 @@ +/* + * 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.util.Log; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.PrintWriter; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +/** + * Class that creates traces of the calls to the UiAutomator API and outputs the + * traces either to logcat or a logfile. Each public method in the UiAutomator + * that needs to be traced should include a call to Tracer.trace in the + * beginning. Tracing is turned off by defualt and needs to be enabled + * explicitly. + * @hide + */ +public class Tracer { + private static final String UNKNOWN_METHOD_STRING = "(unknown method)"; + private static final String UIAUTOMATOR_PACKAGE = "com.android.uiautomator.core"; + private static final int CALLER_LOCATION = 6; + private static final int METHOD_TO_TRACE_LOCATION = 5; + private static final int MIN_STACK_TRACE_LENGTH = 7; + + /** + * Enum that determines where the trace output goes. It can go to either + * logcat, log file or both. + */ + public enum Mode { + NONE, + FILE, + LOGCAT, + ALL + } + + private interface TracerSink { + public void log(String message); + + public void close(); + } + + private class FileSink implements TracerSink { + private PrintWriter mOut; + private SimpleDateFormat mDateFormat; + + public FileSink(File file) throws FileNotFoundException { + mOut = new PrintWriter(file); + mDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US); + } + + public void log(String message) { + mOut.printf("%s %s\n", mDateFormat.format(new Date()), message); + } + + public void close() { + mOut.close(); + } + } + + private class LogcatSink implements TracerSink { + + private static final String LOGCAT_TAG = "UiAutomatorTrace"; + + public void log(String message) { + Log.i(LOGCAT_TAG, message); + } + + public void close() { + // nothing is needed + } + } + + private Mode mCurrentMode = Mode.NONE; + private List<TracerSink> mSinks = new ArrayList<TracerSink>(); + private File mOutputFile; + + private static Tracer mInstance = null; + + /** + * Returns a reference to an instance of the tracer. Useful to set the + * parameters before the trace is collected. + * + * @return + */ + public static Tracer getInstance() { + if (mInstance == null) { + mInstance = new Tracer(); + } + return mInstance; + } + + /** + * Sets where the trace output will go. Can be either be logcat or a file or + * both. Setting this to NONE will turn off tracing. + * + * @param mode + */ + public void setOutputMode(Mode mode) { + closeSinks(); + mCurrentMode = mode; + try { + switch (mode) { + case FILE: + if (mOutputFile == null) { + throw new IllegalArgumentException("Please provide a filename before " + + "attempting write trace to a file"); + } + mSinks.add(new FileSink(mOutputFile)); + break; + case LOGCAT: + mSinks.add(new LogcatSink()); + break; + case ALL: + mSinks.add(new LogcatSink()); + if (mOutputFile == null) { + throw new IllegalArgumentException("Please provide a filename before " + + "attempting write trace to a file"); + } + mSinks.add(new FileSink(mOutputFile)); + break; + default: + break; + } + } catch (FileNotFoundException e) { + Log.w("Tracer", "Could not open log file: " + e.getMessage()); + } + } + + private void closeSinks() { + for (TracerSink sink : mSinks) { + sink.close(); + } + mSinks.clear(); + } + + /** + * Sets the name of the log file where tracing output will be written if the + * tracer is set to write to a file. + * + * @param filename name of the log file. + */ + public void setOutputFilename(String filename) { + mOutputFile = new File(filename); + } + + private void doTrace(Object[] arguments) { + if (mCurrentMode == Mode.NONE) { + return; + } + + String caller = getCaller(); + if (caller == null) { + return; + } + + log(String.format("%s (%s)", caller, join(", ", arguments))); + } + + private void log(String message) { + for (TracerSink sink : mSinks) { + sink.log(message); + } + } + + /** + * Queries whether the tracing is enabled. + * @return true if tracing is enabled, false otherwise. + */ + public boolean isTracingEnabled() { + return mCurrentMode != Mode.NONE; + } + + /** + * Public methods in the UiAutomator should call this function to generate a + * trace. The trace will include the method thats is being called, it's + * arguments and where in the user's code the method is called from. If a + * public method is called internally from UIAutomator then this will not + * output a trace entry. Only calls from outise the UiAutomator package will + * produce output. + * + * Special note about array arguments. You can safely pass arrays of reference types + * to this function. Like String[] or Integer[]. The trace function will print their + * contents by calling toString() on each of the elements. This will not work for + * array of primitive types like int[] or float[]. Before passing them to this function + * convert them to arrays of reference types manually. Example: convert int[] to Integer[]. + * + * @param arguments arguments of the method being traced. + */ + public static void trace(Object... arguments) { + Tracer.getInstance().doTrace(arguments); + } + + private static String join(String separator, Object[] strings) { + if (strings.length == 0) + return ""; + + StringBuilder builder = new StringBuilder(objectToString(strings[0])); + for (int i = 1; i < strings.length; i++) { + builder.append(separator); + builder.append(objectToString(strings[i])); + } + return builder.toString(); + } + + /** + * Special toString method to handle arrays. If the argument is a normal object then this will + * return normal output of obj.toString(). If the argument is an array this will return a + * string representation of the elements of the array. + * + * This method will not work for arrays of primitive types. Arrays of primitive types are + * expected to be converted manually by the caller. If the array is not converter then + * this function will only output "[...]" instead of the contents of the array. + * + * @param obj object to convert to a string + * @return String representation of the object. + */ + private static String objectToString(Object obj) { + if (obj.getClass().isArray()) { + if (obj instanceof Object[]) { + return Arrays.deepToString((Object[])obj); + } else { + return "[...]"; + } + } else { + return obj.toString(); + } + } + + /** + * This method outputs which UiAutomator method was called and where in the + * user code it was called from. If it can't deside which method is called + * it will output "(unknown method)". If the method was called from inside + * the UiAutomator then it returns null. + * + * @return name of the method called and where it was called from. Null if + * method was called from inside UiAutomator. + */ + private static String getCaller() { + StackTraceElement stackTrace[] = Thread.currentThread().getStackTrace(); + if (stackTrace.length < MIN_STACK_TRACE_LENGTH) { + return UNKNOWN_METHOD_STRING; + } + + StackTraceElement caller = stackTrace[METHOD_TO_TRACE_LOCATION]; + StackTraceElement previousCaller = stackTrace[CALLER_LOCATION]; + + if (previousCaller.getClassName().startsWith(UIAUTOMATOR_PACKAGE)) { + return null; + } + + int indexOfDot = caller.getClassName().lastIndexOf('.'); + if (indexOfDot < 0) { + indexOfDot = 0; + } + + if (indexOfDot + 1 >= caller.getClassName().length()) { + return UNKNOWN_METHOD_STRING; + } + + String shortClassName = caller.getClassName().substring(indexOfDot + 1); + return String.format("%s.%s from %s() at %s:%d", shortClassName, caller.getMethodName(), + previousCaller.getMethodName(), previousCaller.getFileName(), + previousCaller.getLineNumber()); + } +} diff --git a/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiAutomatorBridge.java b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiAutomatorBridge.java new file mode 100644 index 0000000..bc5bc8e --- /dev/null +++ b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiAutomatorBridge.java @@ -0,0 +1,143 @@ +package com.android.uiautomator.core; + +import android.accessibilityservice.AccessibilityServiceInfo; +import android.app.UiAutomation; +import android.app.UiAutomation.AccessibilityEventFilter; +import android.app.UiAutomation.OnAccessibilityEventListener; +import android.graphics.Bitmap; +import android.util.Log; +import android.view.Display; +import android.view.InputEvent; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.concurrent.TimeoutException; + +/** + * @hide + */ +public abstract class UiAutomatorBridge { + + private static final String LOG_TAG = UiAutomatorBridge.class.getSimpleName(); + + /** + * This value has the greatest bearing on the appearance of test execution speeds. + * This value is used as the minimum time to wait before considering the UI idle after + * each action. + */ + private static final long QUIET_TIME_TO_BE_CONSIDERD_IDLE_STATE = 500;//ms + + /** + * This is the maximum time the automation will wait for the UI to go idle. Execution + * will resume normally anyway. This is to prevent waiting forever on display updates + * that may be related to spinning wheels or progress updates of sorts etc... + */ + private static final long TOTAL_TIME_TO_WAIT_FOR_IDLE_STATE = 1000 * 10;//ms + + private final UiAutomation mUiAutomation; + + private final InteractionController mInteractionController; + + private final QueryController mQueryController; + + UiAutomatorBridge(UiAutomation uiAutomation) { + mUiAutomation = uiAutomation; + mInteractionController = new InteractionController(this); + mQueryController = new QueryController(this); + } + + InteractionController getInteractionController() { + return mInteractionController; + } + + QueryController getQueryController() { + return mQueryController; + } + + public void setOnAccessibilityEventListener(OnAccessibilityEventListener listener) { + mUiAutomation.setOnAccessibilityEventListener(listener); + } + + public AccessibilityNodeInfo getRootInActiveWindow() { + return mUiAutomation.getRootInActiveWindow(); + } + + public boolean injectInputEvent(InputEvent event, boolean sync) { + return mUiAutomation.injectInputEvent(event, sync); + } + + public boolean setRotation(int rotation) { + return mUiAutomation.setRotation(rotation); + } + + public void setCompressedLayoutHierarchy(boolean compressed) { + AccessibilityServiceInfo info = mUiAutomation.getServiceInfo(); + if (compressed) + info.flags &= ~AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS; + else + info.flags |= AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS; + mUiAutomation.setServiceInfo(info); + } + + public abstract int getRotation(); + + public abstract boolean isScreenOn(); + + public void waitForIdle() { + waitForIdle(TOTAL_TIME_TO_WAIT_FOR_IDLE_STATE); + } + + public void waitForIdle(long timeout) { + try { + mUiAutomation.waitForIdle(QUIET_TIME_TO_BE_CONSIDERD_IDLE_STATE, timeout); + } catch (TimeoutException te) { + Log.w(LOG_TAG, "Could not detect idle state.", te); + } + } + + public AccessibilityEvent executeCommandAndWaitForAccessibilityEvent(Runnable command, + AccessibilityEventFilter filter, long timeoutMillis) throws TimeoutException { + return mUiAutomation.executeAndWaitForEvent(command, + filter, timeoutMillis); + } + + public boolean takeScreenshot(File storePath, int quality) { + Bitmap screenshot = mUiAutomation.takeScreenshot(); + if (screenshot == null) { + return false; + } + BufferedOutputStream bos = null; + try { + bos = new BufferedOutputStream(new FileOutputStream(storePath)); + if (bos != null) { + screenshot.compress(Bitmap.CompressFormat.PNG, quality, bos); + bos.flush(); + } + } catch (IOException ioe) { + Log.e(LOG_TAG, "failed to save screen shot to file", ioe); + return false; + } finally { + if (bos != null) { + try { + bos.close(); + } catch (IOException ioe) { + /* ignore */ + } + } + screenshot.recycle(); + } + return true; + } + + public boolean performGlobalAction(int action) { + return mUiAutomation.performGlobalAction(action); + } + + public abstract Display getDefaultDisplay(); + + public abstract long getSystemLongPressTime(); +} diff --git a/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiCollection.java b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiCollection.java new file mode 100644 index 0000000..e15beb2 --- /dev/null +++ b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiCollection.java @@ -0,0 +1,145 @@ +/* + * 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; + +/** + * Used to enumerate a container's UI elements for the purpose of counting, + * or targeting a sub elements by a child's text or description. + * @since API Level 16 + */ +public class UiCollection extends UiObject { + + /** + * Constructs an instance as described by the selector + * + * @param selector + * @since API Level 16 + */ + public UiCollection(UiSelector selector) { + super(selector); + } + + /** + * Searches for child UI element within the constraints of this UiCollection {@link UiSelector} + * selector. + * + * It looks for any child matching the <code>childPattern</code> argument that has + * a child UI element anywhere within its sub hierarchy that has content-description text. + * The returned UiObject will point at the <code>childPattern</code> instance that matched the + * search and not at the identifying child element that matched the content description.</p> + * + * @param childPattern {@link UiSelector} selector of the child pattern to match and return + * @param text String of the identifying child contents of of the <code>childPattern</code> + * @return {@link UiObject} pointing at and instance of <code>childPattern</code> + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public UiObject getChildByDescription(UiSelector childPattern, String text) + throws UiObjectNotFoundException { + Tracer.trace(childPattern, text); + if (text != null) { + int count = getChildCount(childPattern); + for (int x = 0; x < count; x++) { + UiObject row = getChildByInstance(childPattern, x); + String nodeDesc = row.getContentDescription(); + if(nodeDesc != null && nodeDesc.contains(text)) { + return row; + } + UiObject item = row.getChild(new UiSelector().descriptionContains(text)); + if (item.exists()) { + return row; + } + } + } + throw new UiObjectNotFoundException("for description= \"" + text + "\""); + } + + /** + * Searches for child UI element within the constraints of this UiCollection {@link UiSelector} + * selector. + * + * It looks for any child matching the <code>childPattern</code> argument that has + * a child UI element anywhere within its sub hierarchy that is at the <code>instance</code> + * specified. The operation is performed only on the visible items and no scrolling is performed + * in this case. + * + * @param childPattern {@link UiSelector} selector of the child pattern to match and return + * @param instance int the desired matched instance of this <code>childPattern</code> + * @return {@link UiObject} pointing at and instance of <code>childPattern</code> + * @since API Level 16 + */ + public UiObject getChildByInstance(UiSelector childPattern, int instance) + throws UiObjectNotFoundException { + Tracer.trace(childPattern, instance); + UiSelector patternSelector = UiSelector.patternBuilder(getSelector(), + UiSelector.patternBuilder(childPattern).instance(instance)); + return new UiObject(patternSelector); + } + + /** + * Searches for child UI element within the constraints of this UiCollection {@link UiSelector} + * selector. + * + * It looks for any child matching the <code>childPattern</code> argument that has + * a child UI element anywhere within its sub hierarchy that has text attribute = + * <code>text</code>. The returned UiObject will point at the <code>childPattern</code> + * instance that matched the search and not at the identifying child element that matched the + * text attribute.</p> + * + * @param childPattern {@link UiSelector} selector of the child pattern to match and return + * @param text String of the identifying child contents of of the <code>childPattern</code> + * @return {@link UiObject} pointing at and instance of <code>childPattern</code> + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public UiObject getChildByText(UiSelector childPattern, String text) + throws UiObjectNotFoundException { + Tracer.trace(childPattern, text); + if (text != null) { + int count = getChildCount(childPattern); + for (int x = 0; x < count; x++) { + UiObject row = getChildByInstance(childPattern, x); + String nodeText = row.getText(); + if(text.equals(nodeText)) { + return row; + } + UiObject item = row.getChild(new UiSelector().text(text)); + if (item.exists()) { + return row; + } + } + } + throw new UiObjectNotFoundException("for text= \"" + text + "\""); + } + + /** + * Counts child UI element instances matching the <code>childPattern</code> + * argument. The method returns the number of matching UI elements that are + * currently visible. The count does not include items of a scrollable list + * that are off-screen. + * + * @param childPattern a {@link UiSelector} that represents the matching child UI + * elements to count + * @return the number of matched childPattern under the current {@link UiCollection} + * @since API Level 16 + */ + public int getChildCount(UiSelector childPattern) { + Tracer.trace(childPattern); + UiSelector patternSelector = + UiSelector.patternBuilder(getSelector(), UiSelector.patternBuilder(childPattern)); + return getQueryController().getPatternCount(patternSelector); + } +} diff --git a/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiDevice.java b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiDevice.java new file mode 100644 index 0000000..a930eb4 --- /dev/null +++ b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiDevice.java @@ -0,0 +1,851 @@ +/* + * 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.app.UiAutomation; +import android.app.UiAutomation.AccessibilityEventFilter; +import android.graphics.Point; +import android.os.Build; +import android.os.Environment; +import android.os.RemoteException; +import android.os.SystemClock; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.Display; +import android.view.KeyEvent; +import android.view.Surface; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.TimeoutException; + +/** + * UiDevice provides access to state information about the device. + * You can also use this class to simulate user actions on the device, + * such as pressing the d-pad or pressing the Home and Menu buttons. + * @since API Level 16 + */ +public class UiDevice { + private static final String LOG_TAG = UiDevice.class.getSimpleName(); + + // Sometimes HOME and BACK key presses will generate no events if already on + // home page or there is nothing to go back to, Set low timeouts. + private static final long KEY_PRESS_EVENT_TIMEOUT = 1 * 1000; + + // store for registered UiWatchers + private final HashMap<String, UiWatcher> mWatchers = new HashMap<String, UiWatcher>(); + private final List<String> mWatchersTriggers = new ArrayList<String>(); + + // remember if we're executing in the context of a UiWatcher + private boolean mInWatcherContext = false; + + // provides access the {@link QueryController} and {@link InteractionController} + private UiAutomatorBridge mUiAutomationBridge; + + // reference to self + private static UiDevice sDevice; + + private UiDevice() { + /* hide constructor */ + } + + /** + * @hide + */ + public void initialize(UiAutomatorBridge uiAutomatorBridge) { + mUiAutomationBridge = uiAutomatorBridge; + } + + boolean isInWatcherContext() { + return mInWatcherContext; + } + + /** + * Provides access the {@link QueryController} and {@link InteractionController} + * @return {@link ShellUiAutomatorBridge} + */ + UiAutomatorBridge getAutomatorBridge() { + if (mUiAutomationBridge == null) { + throw new RuntimeException("UiDevice not initialized"); + } + return mUiAutomationBridge; + } + + /** + * Enables or disables layout hierarchy compression. + * + * If compression is enabled, the layout hierarchy derived from the Acessibility + * framework will only contain nodes that are important for uiautomator + * testing. Any unnecessary surrounding layout nodes that make viewing + * and searching the hierarchy inefficient are removed. + * + * @param compressed true to enable compression; else, false to disable + * @since API Level 18 + */ + public void setCompressedLayoutHeirarchy(boolean compressed) { + getAutomatorBridge().setCompressedLayoutHierarchy(compressed); + } + + /** + * Retrieves a singleton instance of UiDevice + * + * @return UiDevice instance + * @since API Level 16 + */ + public static UiDevice getInstance() { + if (sDevice == null) { + sDevice = new UiDevice(); + } + return sDevice; + } + + /** + * Returns the display size in dp (device-independent pixel) + * + * The returned display size is adjusted per screen rotation. Also this will return the actual + * size of the screen, rather than adjusted per system decorations (like status bar). + * + * @return a Point containing the display size in dp + */ + public Point getDisplaySizeDp() { + Tracer.trace(); + Display display = getAutomatorBridge().getDefaultDisplay(); + Point p = new Point(); + display.getRealSize(p); + DisplayMetrics metrics = new DisplayMetrics(); + display.getRealMetrics(metrics); + float dpx = p.x / metrics.density; + float dpy = p.y / metrics.density; + p.x = Math.round(dpx); + p.y = Math.round(dpy); + return p; + } + + /** + * Retrieves the product name of the device. + * + * This method provides information on what type of device the test is running on. This value is + * the same as returned by invoking #adb shell getprop ro.product.name. + * + * @return product name of the device + * @since API Level 17 + */ + public String getProductName() { + Tracer.trace(); + return Build.PRODUCT; + } + + /** + * Retrieves the text from the last UI traversal event received. + * + * You can use this method to read the contents in a WebView container + * because the accessibility framework fires events + * as each text is highlighted. You can write a test to perform + * directional arrow presses to focus on different elements inside a WebView, + * and call this method to get the text from each traversed element. + * If you are testing a view container that can return a reference to a + * Document Object Model (DOM) object, your test should use the view's + * DOM instead. + * + * @return text of the last traversal event, else return an empty string + * @since API Level 16 + */ + public String getLastTraversedText() { + Tracer.trace(); + return getAutomatorBridge().getQueryController().getLastTraversedText(); + } + + /** + * Clears the text from the last UI traversal event. + * See {@link #getLastTraversedText()}. + * @since API Level 16 + */ + public void clearLastTraversedText() { + Tracer.trace(); + getAutomatorBridge().getQueryController().clearLastTraversedText(); + } + + /** + * Simulates a short press on the MENU button. + * @return true if successful, else return false + * @since API Level 16 + */ + public boolean pressMenu() { + Tracer.trace(); + waitForIdle(); + return getAutomatorBridge().getInteractionController().sendKeyAndWaitForEvent( + KeyEvent.KEYCODE_MENU, 0, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED, + KEY_PRESS_EVENT_TIMEOUT); + } + + /** + * Simulates a short press on the BACK button. + * @return true if successful, else return false + * @since API Level 16 + */ + public boolean pressBack() { + Tracer.trace(); + waitForIdle(); + return getAutomatorBridge().getInteractionController().sendKeyAndWaitForEvent( + KeyEvent.KEYCODE_BACK, 0, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED, + KEY_PRESS_EVENT_TIMEOUT); + } + + /** + * Simulates a short press on the HOME button. + * @return true if successful, else return false + * @since API Level 16 + */ + public boolean pressHome() { + Tracer.trace(); + waitForIdle(); + return getAutomatorBridge().getInteractionController().sendKeyAndWaitForEvent( + KeyEvent.KEYCODE_HOME, 0, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED, + KEY_PRESS_EVENT_TIMEOUT); + } + + /** + * Simulates a short press on the SEARCH button. + * @return true if successful, else return false + * @since API Level 16 + */ + public boolean pressSearch() { + Tracer.trace(); + return pressKeyCode(KeyEvent.KEYCODE_SEARCH); + } + + /** + * Simulates a short press on the CENTER button. + * @return true if successful, else return false + * @since API Level 16 + */ + public boolean pressDPadCenter() { + Tracer.trace(); + return pressKeyCode(KeyEvent.KEYCODE_DPAD_CENTER); + } + + /** + * Simulates a short press on the DOWN button. + * @return true if successful, else return false + * @since API Level 16 + */ + public boolean pressDPadDown() { + Tracer.trace(); + return pressKeyCode(KeyEvent.KEYCODE_DPAD_DOWN); + } + + /** + * Simulates a short press on the UP button. + * @return true if successful, else return false + * @since API Level 16 + */ + public boolean pressDPadUp() { + Tracer.trace(); + return pressKeyCode(KeyEvent.KEYCODE_DPAD_UP); + } + + /** + * Simulates a short press on the LEFT button. + * @return true if successful, else return false + * @since API Level 16 + */ + public boolean pressDPadLeft() { + Tracer.trace(); + return pressKeyCode(KeyEvent.KEYCODE_DPAD_LEFT); + } + + /** + * Simulates a short press on the RIGHT button. + * @return true if successful, else return false + * @since API Level 16 + */ + public boolean pressDPadRight() { + Tracer.trace(); + return pressKeyCode(KeyEvent.KEYCODE_DPAD_RIGHT); + } + + /** + * Simulates a short press on the DELETE key. + * @return true if successful, else return false + * @since API Level 16 + */ + public boolean pressDelete() { + Tracer.trace(); + return pressKeyCode(KeyEvent.KEYCODE_DEL); + } + + /** + * Simulates a short press on the ENTER key. + * @return true if successful, else return false + * @since API Level 16 + */ + public boolean pressEnter() { + Tracer.trace(); + return pressKeyCode(KeyEvent.KEYCODE_ENTER); + } + + /** + * Simulates a short press using a key code. + * + * See {@link KeyEvent} + * @return true if successful, else return false + * @since API Level 16 + */ + public boolean pressKeyCode(int keyCode) { + Tracer.trace(keyCode); + waitForIdle(); + return getAutomatorBridge().getInteractionController().sendKey(keyCode, 0); + } + + /** + * Simulates a short press using a key code. + * + * See {@link KeyEvent}. + * @param keyCode the key code of the event. + * @param metaState an integer in which each bit set to 1 represents a pressed meta key + * @return true if successful, else return false + * @since API Level 16 + */ + public boolean pressKeyCode(int keyCode, int metaState) { + Tracer.trace(keyCode, metaState); + waitForIdle(); + return getAutomatorBridge().getInteractionController().sendKey(keyCode, metaState); + } + + /** + * Simulates a short press on the Recent Apps button. + * + * @return true if successful, else return false + * @throws RemoteException + * @since API Level 16 + */ + public boolean pressRecentApps() throws RemoteException { + Tracer.trace(); + waitForIdle(); + return getAutomatorBridge().getInteractionController().toggleRecentApps(); + } + + /** + * Opens the notification shade. + * + * @return true if successful, else return false + * @since API Level 18 + */ + public boolean openNotification() { + Tracer.trace(); + waitForIdle(); + return getAutomatorBridge().getInteractionController().openNotification(); + } + + /** + * Opens the Quick Settings shade. + * + * @return true if successful, else return false + * @since API Level 18 + */ + public boolean openQuickSettings() { + Tracer.trace(); + waitForIdle(); + return getAutomatorBridge().getInteractionController().openQuickSettings(); + } + + /** + * Gets the width of the display, in pixels. The width and height details + * are reported based on the current orientation of the display. + * @return width in pixels or zero on failure + * @since API Level 16 + */ + public int getDisplayWidth() { + Tracer.trace(); + Display display = getAutomatorBridge().getDefaultDisplay(); + Point p = new Point(); + display.getSize(p); + return p.x; + } + + /** + * Gets the height of the display, in pixels. The size is adjusted based + * on the current orientation of the display. + * @return height in pixels or zero on failure + * @since API Level 16 + */ + public int getDisplayHeight() { + Tracer.trace(); + Display display = getAutomatorBridge().getDefaultDisplay(); + Point p = new Point(); + display.getSize(p); + return p.y; + } + + /** + * Perform a click at arbitrary coordinates specified by the user + * + * @param x coordinate + * @param y coordinate + * @return true if the click succeeded else false + * @since API Level 16 + */ + public boolean click(int x, int y) { + Tracer.trace(x, y); + if (x >= getDisplayWidth() || y >= getDisplayHeight()) { + return (false); + } + return getAutomatorBridge().getInteractionController().clickNoSync(x, y); + } + + /** + * Performs a swipe from one coordinate to another using the number of steps + * to determine smoothness and speed. Each step execution is throttled to 5ms + * per step. So for a 100 steps, the swipe will take about 1/2 second to complete. + * + * @param startX + * @param startY + * @param endX + * @param endY + * @param steps is the number of move steps sent to the system + * @return false if the operation fails or the coordinates are invalid + * @since API Level 16 + */ + public boolean swipe(int startX, int startY, int endX, int endY, int steps) { + Tracer.trace(startX, startY, endX, endY, steps); + return getAutomatorBridge().getInteractionController() + .swipe(startX, startY, endX, endY, steps); + } + + /** + * Performs a swipe from one coordinate to another coordinate. You can control + * the smoothness and speed of the swipe by specifying the number of steps. + * Each step execution is throttled to 5 milliseconds per step, so for a 100 + * steps, the swipe will take around 0.5 seconds to complete. + * + * @param startX X-axis value for the starting coordinate + * @param startY Y-axis value for the starting coordinate + * @param endX X-axis value for the ending coordinate + * @param endY Y-axis value for the ending coordinate + * @param steps is the number of steps for the swipe action + * @return true if swipe is performed, false if the operation fails + * or the coordinates are invalid + * @since API Level 18 + */ + public boolean drag(int startX, int startY, int endX, int endY, int steps) { + Tracer.trace(startX, startY, endX, endY, steps); + return getAutomatorBridge().getInteractionController() + .swipe(startX, startY, endX, endY, steps, true); + } + + /** + * Performs a swipe between points in the Point array. Each step execution is throttled + * to 5ms per step. So for a 100 steps, the swipe will take about 1/2 second to complete + * + * @param segments is Point array containing at least one Point object + * @param segmentSteps steps to inject between two Points + * @return true on success + * @since API Level 16 + */ + public boolean swipe(Point[] segments, int segmentSteps) { + Tracer.trace(segments, segmentSteps); + return getAutomatorBridge().getInteractionController().swipe(segments, segmentSteps); + } + + /** + * Waits for the current application to idle. + * Default wait timeout is 10 seconds + * @since API Level 16 + */ + public void waitForIdle() { + Tracer.trace(); + waitForIdle(Configurator.getInstance().getWaitForIdleTimeout()); + } + + /** + * Waits for the current application to idle. + * @param timeout in milliseconds + * @since API Level 16 + */ + public void waitForIdle(long timeout) { + Tracer.trace(timeout); + getAutomatorBridge().waitForIdle(timeout); + } + + /** + * Retrieves the last activity to report accessibility events. + * @deprecated The results returned should be considered unreliable + * @return String name of activity + * @since API Level 16 + */ + @Deprecated + public String getCurrentActivityName() { + Tracer.trace(); + return getAutomatorBridge().getQueryController().getCurrentActivityName(); + } + + /** + * Retrieves the name of the last package to report accessibility events. + * @return String name of package + * @since API Level 16 + */ + public String getCurrentPackageName() { + Tracer.trace(); + return getAutomatorBridge().getQueryController().getCurrentPackageName(); + } + + /** + * Registers a {@link UiWatcher} to run automatically when the testing framework is unable to + * find a match using a {@link UiSelector}. See {@link #runWatchers()} + * + * @param name to register the UiWatcher + * @param watcher {@link UiWatcher} + * @since API Level 16 + */ + public void registerWatcher(String name, UiWatcher watcher) { + Tracer.trace(name, watcher); + if (mInWatcherContext) { + throw new IllegalStateException("Cannot register new watcher from within another"); + } + mWatchers.put(name, watcher); + } + + /** + * Removes a previously registered {@link UiWatcher}. + * + * See {@link #registerWatcher(String, UiWatcher)} + * @param name used to register the UiWatcher + * @since API Level 16 + */ + public void removeWatcher(String name) { + Tracer.trace(name); + if (mInWatcherContext) { + throw new IllegalStateException("Cannot remove a watcher from within another"); + } + mWatchers.remove(name); + } + + /** + * This method forces all registered watchers to run. + * See {@link #registerWatcher(String, UiWatcher)} + * @since API Level 16 + */ + public void runWatchers() { + Tracer.trace(); + if (mInWatcherContext) { + return; + } + + for (String watcherName : mWatchers.keySet()) { + UiWatcher watcher = mWatchers.get(watcherName); + if (watcher != null) { + try { + mInWatcherContext = true; + if (watcher.checkForCondition()) { + setWatcherTriggered(watcherName); + } + } catch (Exception e) { + Log.e(LOG_TAG, "Exceuting watcher: " + watcherName, e); + } finally { + mInWatcherContext = false; + } + } + } + } + + /** + * Resets a {@link UiWatcher} that has been triggered. + * If a UiWatcher runs and its {@link UiWatcher#checkForCondition()} call + * returned <code>true</code>, then the UiWatcher is considered triggered. + * See {@link #registerWatcher(String, UiWatcher)} + * @since API Level 16 + */ + public void resetWatcherTriggers() { + Tracer.trace(); + mWatchersTriggers.clear(); + } + + /** + * Checks if a specific registered {@link UiWatcher} has triggered. + * See {@link #registerWatcher(String, UiWatcher)}. If a UiWatcher runs and its + * {@link UiWatcher#checkForCondition()} call returned <code>true</code>, then + * the UiWatcher is considered triggered. This is helpful if a watcher is detecting errors + * from ANR or crash dialogs and the test needs to know if a UiWatcher has been triggered. + * + * @param watcherName + * @return true if triggered else false + * @since API Level 16 + */ + public boolean hasWatcherTriggered(String watcherName) { + Tracer.trace(watcherName); + return mWatchersTriggers.contains(watcherName); + } + + /** + * Checks if any registered {@link UiWatcher} have triggered. + * + * See {@link #registerWatcher(String, UiWatcher)} + * See {@link #hasWatcherTriggered(String)} + * @since API Level 16 + */ + public boolean hasAnyWatcherTriggered() { + Tracer.trace(); + return mWatchersTriggers.size() > 0; + } + + /** + * Used internally by this class to set a {@link UiWatcher} state as triggered. + * @param watcherName + */ + private void setWatcherTriggered(String watcherName) { + Tracer.trace(watcherName); + if (!hasWatcherTriggered(watcherName)) { + mWatchersTriggers.add(watcherName); + } + } + + /** + * Check if the device is in its natural orientation. This is determined by checking if the + * orientation is at 0 or 180 degrees. + * @return true if it is in natural orientation + * @since API Level 17 + */ + public boolean isNaturalOrientation() { + Tracer.trace(); + waitForIdle(); + int ret = getAutomatorBridge().getRotation(); + return ret == UiAutomation.ROTATION_FREEZE_0 || + ret == UiAutomation.ROTATION_FREEZE_180; + } + + /** + * Returns the current rotation of the display, as defined in {@link Surface} + * @since API Level 17 + */ + public int getDisplayRotation() { + Tracer.trace(); + waitForIdle(); + return getAutomatorBridge().getRotation(); + } + + /** + * Disables the sensors and freezes the device rotation at its + * current rotation state. + * @throws RemoteException + * @since API Level 16 + */ + public void freezeRotation() throws RemoteException { + Tracer.trace(); + getAutomatorBridge().getInteractionController().freezeRotation(); + } + + /** + * Re-enables the sensors and un-freezes the device rotation allowing its contents + * to rotate with the device physical rotation. During a test execution, it is best to + * keep the device frozen in a specific orientation until the test case execution has completed. + * @throws RemoteException + */ + public void unfreezeRotation() throws RemoteException { + Tracer.trace(); + getAutomatorBridge().getInteractionController().unfreezeRotation(); + } + + /** + * Simulates orienting the device to the left and also freezes rotation + * by disabling the sensors. + * + * If you want to un-freeze the rotation and re-enable the sensors + * see {@link #unfreezeRotation()}. + * @throws RemoteException + * @since API Level 17 + */ + public void setOrientationLeft() throws RemoteException { + Tracer.trace(); + getAutomatorBridge().getInteractionController().setRotationLeft(); + waitForIdle(); // we don't need to check for idle on entry for this. We'll sync on exit + } + + /** + * Simulates orienting the device to the right and also freezes rotation + * by disabling the sensors. + * + * If you want to un-freeze the rotation and re-enable the sensors + * see {@link #unfreezeRotation()}. + * @throws RemoteException + * @since API Level 17 + */ + public void setOrientationRight() throws RemoteException { + Tracer.trace(); + getAutomatorBridge().getInteractionController().setRotationRight(); + waitForIdle(); // we don't need to check for idle on entry for this. We'll sync on exit + } + + /** + * Simulates orienting the device into its natural orientation and also freezes rotation + * by disabling the sensors. + * + * If you want to un-freeze the rotation and re-enable the sensors + * see {@link #unfreezeRotation()}. + * @throws RemoteException + * @since API Level 17 + */ + public void setOrientationNatural() throws RemoteException { + Tracer.trace(); + getAutomatorBridge().getInteractionController().setRotationNatural(); + waitForIdle(); // we don't need to check for idle on entry for this. We'll sync on exit + } + + /** + * This method simulates pressing the power button if the screen is OFF else + * it does nothing if the screen is already ON. + * + * If the screen was OFF and it just got turned ON, this method will insert a 500ms delay + * to allow the device time to wake up and accept input. + * @throws RemoteException + * @since API Level 16 + */ + public void wakeUp() throws RemoteException { + Tracer.trace(); + if(getAutomatorBridge().getInteractionController().wakeDevice()) { + // sync delay to allow the window manager to start accepting input + // after the device is awakened. + SystemClock.sleep(500); + } + } + + /** + * Checks the power manager if the screen is ON. + * + * @return true if the screen is ON else false + * @throws RemoteException + * @since API Level 16 + */ + public boolean isScreenOn() throws RemoteException { + Tracer.trace(); + return getAutomatorBridge().getInteractionController().isScreenOn(); + } + + /** + * This method simply presses the power button if the screen is ON else + * it does nothing if the screen is already OFF. + * + * @throws RemoteException + * @since API Level 16 + */ + public void sleep() throws RemoteException { + Tracer.trace(); + getAutomatorBridge().getInteractionController().sleepDevice(); + } + + /** + * Helper method used for debugging to dump the current window's layout hierarchy. + * The file root location is /data/local/tmp + * + * @param fileName + * @since API Level 16 + */ + public void dumpWindowHierarchy(String fileName) { + Tracer.trace(fileName); + AccessibilityNodeInfo root = + getAutomatorBridge().getQueryController().getAccessibilityRootNode(); + if(root != null) { + Display display = getAutomatorBridge().getDefaultDisplay(); + Point size = new Point(); + display.getSize(size); + AccessibilityNodeInfoDumper.dumpWindowToFile(root, + new File(new File(Environment.getDataDirectory(), "local/tmp"), fileName), + display.getRotation(), size.x, size.y); + } + } + + /** + * Waits for a window content update event to occur. + * + * If a package name for the window is specified, but the current window + * does not have the same package name, the function returns immediately. + * + * @param packageName the specified window package name (can be <code>null</code>). + * If <code>null</code>, a window update from any front-end window will end the wait + * @param timeout the timeout for the wait + * + * @return true if a window update occurred, false if timeout has elapsed or if the current + * window does not have the specified package name + * @since API Level 16 + */ + public boolean waitForWindowUpdate(final String packageName, long timeout) { + Tracer.trace(packageName, timeout); + if (packageName != null) { + if (!packageName.equals(getCurrentPackageName())) { + return false; + } + } + Runnable emptyRunnable = new Runnable() { + @Override + public void run() { + } + }; + AccessibilityEventFilter checkWindowUpdate = new AccessibilityEventFilter() { + @Override + public boolean accept(AccessibilityEvent t) { + if (t.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) { + return packageName == null || packageName.equals(t.getPackageName()); + } + return false; + } + }; + try { + getAutomatorBridge().executeCommandAndWaitForAccessibilityEvent( + emptyRunnable, checkWindowUpdate, timeout); + } catch (TimeoutException e) { + return false; + } catch (Exception e) { + Log.e(LOG_TAG, "waitForWindowUpdate: general exception from bridge", e); + return false; + } + return true; + } + + /** + * Take a screenshot of current window and store it as PNG + * + * Default scale of 1.0f (original size) and 90% quality is used + * The screenshot is adjusted per screen rotation + * + * @param storePath where the PNG should be written to + * @return true if screen shot is created successfully, false otherwise + * @since API Level 17 + */ + public boolean takeScreenshot(File storePath) { + Tracer.trace(storePath); + return takeScreenshot(storePath, 1.0f, 90); + } + + /** + * Take a screenshot of current window and store it as PNG + * + * The screenshot is adjusted per screen rotation + * + * @param storePath where the PNG should be written to + * @param scale scale the screenshot down if needed; 1.0f for original size + * @param quality quality of the PNG compression; range: 0-100 + * @return true if screen shot is created successfully, false otherwise + * @since API Level 17 + */ + public boolean takeScreenshot(File storePath, float scale, int quality) { + Tracer.trace(storePath, scale, quality); + return getAutomatorBridge().takeScreenshot(storePath, quality); + } +} diff --git a/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiObject.java b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiObject.java new file mode 100644 index 0000000..4bb99cd --- /dev/null +++ b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiObject.java @@ -0,0 +1,1083 @@ +/* + * 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.graphics.Point; +import android.graphics.Rect; +import android.os.SystemClock; +import android.util.Log; +import android.view.KeyEvent; +import android.view.MotionEvent.PointerCoords; +import android.view.accessibility.AccessibilityNodeInfo; + +/** + * A UiObject is a representation of a view. It is not in any way directly bound to a + * view as an object reference. A UiObject contains information to help it + * locate a matching view at runtime based on the {@link UiSelector} properties specified in + * its constructor. Once you create an instance of a UiObject, it can + * be reused for different views that match the selector criteria. + * @since API Level 16 + */ +public class UiObject { + private static final String LOG_TAG = UiObject.class.getSimpleName(); + /** + * @since API Level 16 + * @deprecated use {@link Configurator#setWaitForSelectorTimeout(long)} + **/ + @Deprecated + protected static final long WAIT_FOR_SELECTOR_TIMEOUT = 10 * 1000; + /** + * @since API Level 16 + **/ + protected static final long WAIT_FOR_SELECTOR_POLL = 1000; + // set a default timeout to 5.5s, since ANR threshold is 5s + /** + * @since API Level 16 + **/ + protected static final long WAIT_FOR_WINDOW_TMEOUT = 5500; + /** + * @since API Level 16 + **/ + protected static final int SWIPE_MARGIN_LIMIT = 5; + /** + * @since API Level 17 + * @deprecated use {@link Configurator#setScrollAcknowledgmentTimeout(long)} + **/ + @Deprecated + protected static final long WAIT_FOR_EVENT_TMEOUT = 3 * 1000; + /** + * @since API Level 18 + **/ + protected static final int FINGER_TOUCH_HALF_WIDTH = 20; + + private final UiSelector mSelector; + + private final Configurator mConfig = Configurator.getInstance(); + + /** + * Constructs a UiObject to represent a view that matches the specified + * selector criteria. + * @param selector + * @since API Level 16 + */ + public UiObject(UiSelector selector) { + mSelector = selector; + } + + /** + * Debugging helper. A test can dump the properties of a selector as a string + * to its logs if needed. <code>getSelector().toString();</code> + * + * @return {@link UiSelector} + * @since API Level 16 + */ + public final UiSelector getSelector() { + Tracer.trace(); + return new UiSelector(mSelector); + } + + /** + * Retrieves the {@link QueryController} to translate a {@link UiSelector} selector + * into an {@link AccessibilityNodeInfo}. + * + * @return {@link QueryController} + */ + QueryController getQueryController() { + return UiDevice.getInstance().getAutomatorBridge().getQueryController(); + } + + /** + * Retrieves the {@link InteractionController} to perform finger actions such as tapping, + * swiping, or entering text. + * + * @return {@link InteractionController} + */ + InteractionController getInteractionController() { + return UiDevice.getInstance().getAutomatorBridge().getInteractionController(); + } + + /** + * Creates a new UiObject for a child view that is under the present UiObject. + * + * @param selector for child view to match + * @return a new UiObject representing the child view + * @since API Level 16 + */ + public UiObject getChild(UiSelector selector) throws UiObjectNotFoundException { + Tracer.trace(selector); + return new UiObject(getSelector().childSelector(selector)); + } + + /** + * Creates a new UiObject for a sibling view or a child of the sibling view, + * relative to the present UiObject. + * + * @param selector for a sibling view or children of the sibling view + * @return a new UiObject representing the matched view + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public UiObject getFromParent(UiSelector selector) throws UiObjectNotFoundException { + Tracer.trace(selector); + return new UiObject(getSelector().fromParent(selector)); + } + + /** + * Counts the child views immediately under the present UiObject. + * + * @return the count of child views. + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public int getChildCount() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + return node.getChildCount(); + } + + /** + * Finds a matching UI element in the accessibility hierarchy, by + * using the selector for this UiObject. + * + * @param timeout in milliseconds + * @return AccessibilityNodeInfo if found else null + * @since API Level 16 + */ + protected AccessibilityNodeInfo findAccessibilityNodeInfo(long timeout) { + AccessibilityNodeInfo node = null; + long startMills = SystemClock.uptimeMillis(); + long currentMills = 0; + while (currentMills <= timeout) { + node = getQueryController().findAccessibilityNodeInfo(getSelector()); + if (node != null) { + break; + } else { + // does nothing if we're reentering another runWatchers() + UiDevice.getInstance().runWatchers(); + } + currentMills = SystemClock.uptimeMillis() - startMills; + if(timeout > 0) { + SystemClock.sleep(WAIT_FOR_SELECTOR_POLL); + } + } + return node; + } + + /** + * Drags this object to a destination UiObject. + * The number of steps specified in your input parameter can influence the + * drag speed, and varying speeds may impact the results. Consider + * evaluating different speeds when using this method in your tests. + * + * @param destObj the destination UiObject. + * @param steps usually 40 steps. You can increase or decrease the steps to change the speed. + * @return true if successful + * @throws UiObjectNotFoundException + * @since API Level 18 + */ + public boolean dragTo(UiObject destObj, int steps) throws UiObjectNotFoundException { + Rect srcRect = getVisibleBounds(); + Rect dstRect = destObj.getVisibleBounds(); + return getInteractionController().swipe(srcRect.centerX(), srcRect.centerY(), + dstRect.centerX(), dstRect.centerY(), steps, true); + } + + /** + * Drags this object to arbitrary coordinates. + * The number of steps specified in your input parameter can influence the + * drag speed, and varying speeds may impact the results. Consider + * evaluating different speeds when using this method in your tests. + * + * @param destX the X-axis coordinate. + * @param destY the Y-axis coordinate. + * @param steps usually 40 steps. You can increase or decrease the steps to change the speed. + * @return true if successful + * @throws UiObjectNotFoundException + * @since API Level 18 + */ + public boolean dragTo(int destX, int destY, int steps) throws UiObjectNotFoundException { + Rect srcRect = getVisibleBounds(); + return getInteractionController().swipe(srcRect.centerX(), srcRect.centerY(), destX, destY, + steps, true); + } + + /** + * Performs the swipe up action on the UiObject. + * See also: + * <ul> + * <li>{@link UiScrollable#scrollToBeginning(int)}</li> + * <li>{@link UiScrollable#scrollToEnd(int)}</li> + * <li>{@link UiScrollable#scrollBackward()}</li> + * <li>{@link UiScrollable#scrollForward()}</li> + * </ul> + * + * @param steps indicates the number of injected move steps into the system. Steps are + * injected about 5ms apart. So a 100 steps may take about 1/2 second to complete. + * @return true of successful + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public boolean swipeUp(int steps) throws UiObjectNotFoundException { + Tracer.trace(steps); + Rect rect = getVisibleBounds(); + if(rect.height() <= SWIPE_MARGIN_LIMIT * 2) + return false; // too small to swipe + return getInteractionController().swipe(rect.centerX(), + rect.bottom - SWIPE_MARGIN_LIMIT, rect.centerX(), rect.top + SWIPE_MARGIN_LIMIT, + steps); + } + + /** + * Performs the swipe down action on the UiObject. + * The swipe gesture can be performed over any surface. The targeted + * UI element does not need to be scrollable. + * See also: + * <ul> + * <li>{@link UiScrollable#scrollToBeginning(int)}</li> + * <li>{@link UiScrollable#scrollToEnd(int)}</li> + * <li>{@link UiScrollable#scrollBackward()}</li> + * <li>{@link UiScrollable#scrollForward()}</li> + * </ul> + * + * @param steps indicates the number of injected move steps into the system. Steps are + * injected about 5ms apart. So a 100 steps may take about 1/2 second to complete. + * @return true if successful + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public boolean swipeDown(int steps) throws UiObjectNotFoundException { + Tracer.trace(steps); + Rect rect = getVisibleBounds(); + if(rect.height() <= SWIPE_MARGIN_LIMIT * 2) + return false; // too small to swipe + return getInteractionController().swipe(rect.centerX(), + rect.top + SWIPE_MARGIN_LIMIT, rect.centerX(), + rect.bottom - SWIPE_MARGIN_LIMIT, steps); + } + + /** + * Performs the swipe left action on the UiObject. + * The swipe gesture can be performed over any surface. The targeted + * UI element does not need to be scrollable. + * See also: + * <ul> + * <li>{@link UiScrollable#scrollToBeginning(int)}</li> + * <li>{@link UiScrollable#scrollToEnd(int)}</li> + * <li>{@link UiScrollable#scrollBackward()}</li> + * <li>{@link UiScrollable#scrollForward()}</li> + * </ul> + * + * @param steps indicates the number of injected move steps into the system. Steps are + * injected about 5ms apart. So a 100 steps may take about 1/2 second to complete. + * @return true if successful + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public boolean swipeLeft(int steps) throws UiObjectNotFoundException { + Tracer.trace(steps); + Rect rect = getVisibleBounds(); + if(rect.width() <= SWIPE_MARGIN_LIMIT * 2) + return false; // too small to swipe + return getInteractionController().swipe(rect.right - SWIPE_MARGIN_LIMIT, + rect.centerY(), rect.left + SWIPE_MARGIN_LIMIT, rect.centerY(), steps); + } + + /** + * Performs the swipe right action on the UiObject. + * The swipe gesture can be performed over any surface. The targeted + * UI element does not need to be scrollable. + * See also: + * <ul> + * <li>{@link UiScrollable#scrollToBeginning(int)}</li> + * <li>{@link UiScrollable#scrollToEnd(int)}</li> + * <li>{@link UiScrollable#scrollBackward()}</li> + * <li>{@link UiScrollable#scrollForward()}</li> + * </ul> + * + * @param steps indicates the number of injected move steps into the system. Steps are + * injected about 5ms apart. So a 100 steps may take about 1/2 second to complete. + * @return true if successful + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public boolean swipeRight(int steps) throws UiObjectNotFoundException { + Tracer.trace(steps); + Rect rect = getVisibleBounds(); + if(rect.width() <= SWIPE_MARGIN_LIMIT * 2) + return false; // too small to swipe + return getInteractionController().swipe(rect.left + SWIPE_MARGIN_LIMIT, + rect.centerY(), rect.right - SWIPE_MARGIN_LIMIT, rect.centerY(), steps); + } + + /** + * Finds the visible bounds of a partially visible UI element + * + * @param node + * @return null if node is null, else a Rect containing visible bounds + */ + private Rect getVisibleBounds(AccessibilityNodeInfo node) { + if (node == null) { + return null; + } + + // targeted node's bounds + int w = UiDevice.getInstance().getDisplayWidth(); + int h = UiDevice.getInstance().getDisplayHeight(); + Rect nodeRect = AccessibilityNodeInfoHelper.getVisibleBoundsInScreen(node, w, h); + + // is the targeted node within a scrollable container? + AccessibilityNodeInfo scrollableParentNode = getScrollableParent(node); + if(scrollableParentNode == null) { + // nothing to adjust for so return the node's Rect as is + return nodeRect; + } + + // Scrollable parent's visible bounds + Rect parentRect = AccessibilityNodeInfoHelper + .getVisibleBoundsInScreen(scrollableParentNode, w, h); + // adjust for partial clipping of targeted by parent node if required + nodeRect.intersect(parentRect); + return nodeRect; + } + + /** + * Walks up the layout hierarchy to find a scrollable parent. A scrollable parent + * indicates that this node might be in a container where it is partially + * visible due to scrolling. In this case, its clickable center might not be visible and + * the click coordinates should be adjusted. + * + * @param node + * @return The accessibility node info. + */ + private AccessibilityNodeInfo getScrollableParent(AccessibilityNodeInfo node) { + AccessibilityNodeInfo parent = node; + while(parent != null) { + parent = parent.getParent(); + if (parent != null && parent.isScrollable()) { + return parent; + } + } + return null; + } + + /** + * Performs a click at the center of the visible bounds of the UI element represented + * by this UiObject. + * + * @return true id successful else false + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public boolean click() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + Rect rect = getVisibleBounds(node); + return getInteractionController().clickAndSync(rect.centerX(), rect.centerY(), + mConfig.getActionAcknowledgmentTimeout()); + } + + /** + * Waits for window transitions that would typically take longer than the + * usual default timeouts. + * See {@link #clickAndWaitForNewWindow(long)} + * + * @return true if the event was triggered, else false + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public boolean clickAndWaitForNewWindow() throws UiObjectNotFoundException { + Tracer.trace(); + return clickAndWaitForNewWindow(WAIT_FOR_WINDOW_TMEOUT); + } + + /** + * Performs a click at the center of the visible bounds of the UI element represented + * by this UiObject and waits for window transitions. + * + * This method differ from {@link UiObject#click()} only in that this method waits for a + * a new window transition as a result of the click. Some examples of a window transition: + * <li>launching a new activity</li> + * <li>bringing up a pop-up menu</li> + * <li>bringing up a dialog</li> + * + * @param timeout timeout before giving up on waiting for a new window + * @return true if the event was triggered, else false + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public boolean clickAndWaitForNewWindow(long timeout) throws UiObjectNotFoundException { + Tracer.trace(timeout); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + Rect rect = getVisibleBounds(node); + return getInteractionController().clickAndWaitForNewWindow(rect.centerX(), rect.centerY(), + mConfig.getActionAcknowledgmentTimeout()); + } + + /** + * Clicks the top and left corner of the UI element + * + * @return true on success + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public boolean clickTopLeft() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + Rect rect = getVisibleBounds(node); + return getInteractionController().clickNoSync(rect.left + 5, rect.top + 5); + } + + /** + * Long clicks bottom and right corner of the UI element + * + * @return true if operation was successful + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public boolean longClickBottomRight() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + Rect rect = getVisibleBounds(node); + return getInteractionController().longTapNoSync(rect.right - 5, rect.bottom - 5); + } + + /** + * Clicks the bottom and right corner of the UI element + * + * @return true on success + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public boolean clickBottomRight() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + Rect rect = getVisibleBounds(node); + return getInteractionController().clickNoSync(rect.right - 5, rect.bottom - 5); + } + + /** + * Long clicks the center of the visible bounds of the UI element + * + * @return true if operation was successful + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public boolean longClick() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + Rect rect = getVisibleBounds(node); + return getInteractionController().longTapNoSync(rect.centerX(), rect.centerY()); + } + + /** + * Long clicks on the top and left corner of the UI element + * + * @return true if operation was successful + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public boolean longClickTopLeft() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + Rect rect = getVisibleBounds(node); + return getInteractionController().longTapNoSync(rect.left + 5, rect.top + 5); + } + + /** + * Reads the <code>text</code> property of the UI element + * + * @return text value of the current node represented by this UiObject + * @throws UiObjectNotFoundException if no match could be found + * @since API Level 16 + */ + public String getText() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + String retVal = safeStringReturn(node.getText()); + Log.d(LOG_TAG, String.format("getText() = %s", retVal)); + return retVal; + } + + /** + * Retrieves the <code>className</code> property of the UI element. + * + * @return class name of the current node represented by this UiObject + * @throws UiObjectNotFoundException if no match was found + * @since API Level 18 + */ + public String getClassName() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + String retVal = safeStringReturn(node.getClassName()); + Log.d(LOG_TAG, String.format("getClassName() = %s", retVal)); + return retVal; + } + + /** + * Reads the <code>content_desc</code> property of the UI element + * + * @return value of node attribute "content_desc" + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public String getContentDescription() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + return safeStringReturn(node.getContentDescription()); + } + + /** + * Sets the text in an editable field, after clearing the field's content. + * + * The {@link UiSelector} selector of this object must reference a UI element that is editable. + * + * When you call this method, the method first simulates a {@link #click()} on + * editable field to set focus. The method then clears the field's contents + * and injects your specified text into the field. + * + * If you want to capture the original contents of the field, call {@link #getText()} first. + * You can then modify the text and use this method to update the field. + * + * @param text string to set + * @return true if operation is successful + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public boolean setText(String text) throws UiObjectNotFoundException { + Tracer.trace(text); + clearTextField(); + return getInteractionController().sendText(text); + } + + /** + * Clears the existing text contents in an editable field. + * + * The {@link UiSelector} of this object must reference a UI element that is editable. + * + * When you call this method, the method first sets focus at the start edge of the field. + * The method then simulates a long-press to select the existing text, and deletes the + * selected text. + * + * If a "Select-All" option is displayed, the method will automatically attempt to use it + * to ensure full text selection. + * + * Note that it is possible that not all the text in the field is selected; for example, + * if the text contains separators such as spaces, slashes, at symbol etc. + * Also, not all editable fields support the long-press functionality. + * + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public void clearTextField() throws UiObjectNotFoundException { + Tracer.trace(); + // long click left + center + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + Rect rect = getVisibleBounds(node); + 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)) + selectAll.click(); + // wait for the selection + SystemClock.sleep(250); + // delete it + getInteractionController().sendKey(KeyEvent.KEYCODE_DEL, 0); + } + + /** + * Check if the UI element's <code>checked</code> property is currently true + * + * @return true if it is else false + * @since API Level 16 + */ + public boolean isChecked() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + return node.isChecked(); + } + + /** + * Checks if the UI element's <code>selected</code> property is currently true. + * + * @return true if it is else false + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public boolean isSelected() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + return node.isSelected(); + } + + /** + * Checks if the UI element's <code>checkable</code> property is currently true. + * + * @return true if it is else false + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public boolean isCheckable() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + return node.isCheckable(); + } + + /** + * Checks if the UI element's <code>enabled</code> property is currently true. + * + * @return true if it is else false + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public boolean isEnabled() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + return node.isEnabled(); + } + + /** + * Checks if the UI element's <code>clickable</code> property is currently true. + * + * @return true if it is else false + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public boolean isClickable() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + return node.isClickable(); + } + + /** + * Check if the UI element's <code>focused</code> property is currently true + * + * @return true if it is else false + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public boolean isFocused() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + return node.isFocused(); + } + + /** + * Check if the UI element's <code>focusable</code> property is currently true. + * + * @return true if it is else false + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public boolean isFocusable() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + return node.isFocusable(); + } + + /** + * Check if the view's <code>scrollable</code> property is currently true + * + * @return true if it is else false + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public boolean isScrollable() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + return node.isScrollable(); + } + + /** + * Check if the view's <code>long-clickable</code> property is currently true + * + * @return true if it is else false + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public boolean isLongClickable() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + return node.isLongClickable(); + } + + /** + * Reads the view's <code>package</code> property + * + * @return true if it is else false + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public String getPackageName() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + return safeStringReturn(node.getPackageName()); + } + + /** + * Returns the visible bounds of the view. + * + * If a portion of the view is visible, only the bounds of the visible portion are + * reported. + * + * @return Rect + * @throws UiObjectNotFoundException + * @see {@link #getBounds()} + * @since API Level 17 + */ + public Rect getVisibleBounds() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + return getVisibleBounds(node); + } + + /** + * Returns the view's <code>bounds</code> property. See {@link #getVisibleBounds()} + * + * @return Rect + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public Rect getBounds() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + Rect nodeRect = new Rect(); + node.getBoundsInScreen(nodeRect); + + return nodeRect; + } + + /** + * Waits a specified length of time for a view to become visible. + * + * This method waits until the view becomes visible on the display, or + * until the timeout has elapsed. You can use this method in situations where + * the content that you want to select is not immediately displayed. + * + * @param timeout the amount of time to wait (in milliseconds) + * @return true if the view is displayed, else false if timeout elapsed while waiting + * @since API Level 16 + */ + public boolean waitForExists(long timeout) { + Tracer.trace(timeout); + if(findAccessibilityNodeInfo(timeout) != null) { + return true; + } + return false; + } + + /** + * Waits a specified length of time for a view to become undetectable. + * + * This method waits until a view is no longer matchable, or until the + * timeout has elapsed. + * + * A view becomes undetectable when the {@link UiSelector} of the object is + * unable to find a match because the element has either changed its state or is no + * longer displayed. + * + * You can use this method when attempting to wait for some long operation + * to compete, such as downloading a large file or connecting to a remote server. + * + * @param timeout time to wait (in milliseconds) + * @return true if the element is gone before timeout elapsed, else false if timeout elapsed + * but a matching element is still found. + * @since API Level 16 + */ + public boolean waitUntilGone(long timeout) { + Tracer.trace(timeout); + long startMills = SystemClock.uptimeMillis(); + long currentMills = 0; + while (currentMills <= timeout) { + if(findAccessibilityNodeInfo(0) == null) + return true; + currentMills = SystemClock.uptimeMillis() - startMills; + if(timeout > 0) + SystemClock.sleep(WAIT_FOR_SELECTOR_POLL); + } + return false; + } + + /** + * Check if view exists. + * + * This methods performs a {@link #waitForExists(long)} with zero timeout. This + * basically returns immediately whether the view represented by this UiObject + * exists or not. If you need to wait longer for this view, then see + * {@link #waitForExists(long)}. + * + * @return true if the view represented by this UiObject does exist + * @since API Level 16 + */ + public boolean exists() { + Tracer.trace(); + return waitForExists(0); + } + + private String safeStringReturn(CharSequence cs) { + if(cs == null) + return ""; + return cs.toString(); + } + + /** + * Performs a two-pointer gesture, where each pointer moves diagonally + * opposite across the other, from the center out towards the edges of the + * this UiObject. + * @param percent percentage of the object's diagonal length for the pinch gesture + * @param steps the number of steps for the gesture. Steps are injected + * about 5 milliseconds apart, so 100 steps may take around 0.5 seconds to complete. + * @return <code>true</code> if all touch events for this gesture are injected successfully, + * <code>false</code> otherwise + * @throws UiObjectNotFoundException + * @since API Level 18 + */ + public boolean pinchOut(int percent, int steps) throws UiObjectNotFoundException { + // make value between 1 and 100 + percent = (percent < 0) ? 1 : (percent > 100) ? 100 : percent; + float percentage = percent / 100f; + + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if (node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + + Rect rect = getVisibleBounds(node); + if (rect.width() <= FINGER_TOUCH_HALF_WIDTH * 2) + throw new IllegalStateException("Object width is too small for operation"); + + // start from the same point at the center of the control + Point startPoint1 = new Point(rect.centerX() - FINGER_TOUCH_HALF_WIDTH, rect.centerY()); + Point startPoint2 = new Point(rect.centerX() + FINGER_TOUCH_HALF_WIDTH, rect.centerY()); + + // End at the top-left and bottom-right corners of the control + Point endPoint1 = new Point(rect.centerX() - (int)((rect.width()/2) * percentage), + rect.centerY()); + Point endPoint2 = new Point(rect.centerX() + (int)((rect.width()/2) * percentage), + rect.centerY()); + + return performTwoPointerGesture(startPoint1, startPoint2, endPoint1, endPoint2, steps); + } + + /** + * Performs a two-pointer gesture, where each pointer moves diagonally + * toward the other, from the edges to the center of this UiObject . + * @param percent percentage of the object's diagonal length for the pinch gesture + * @param steps the number of steps for the gesture. Steps are injected + * about 5 milliseconds apart, so 100 steps may take around 0.5 seconds to complete. + * @return <code>true</code> if all touch events for this gesture are injected successfully, + * <code>false</code> otherwise + * @throws UiObjectNotFoundException + * @since API Level 18 + */ + public boolean pinchIn(int percent, int steps) throws UiObjectNotFoundException { + // make value between 1 and 100 + percent = (percent < 0) ? 0 : (percent > 100) ? 100 : percent; + float percentage = percent / 100f; + + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if (node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + + Rect rect = getVisibleBounds(node); + if (rect.width() <= FINGER_TOUCH_HALF_WIDTH * 2) + throw new IllegalStateException("Object width is too small for operation"); + + Point startPoint1 = new Point(rect.centerX() - (int)((rect.width()/2) * percentage), + rect.centerY()); + Point startPoint2 = new Point(rect.centerX() + (int)((rect.width()/2) * percentage), + rect.centerY()); + + Point endPoint1 = new Point(rect.centerX() - FINGER_TOUCH_HALF_WIDTH, rect.centerY()); + Point endPoint2 = new Point(rect.centerX() + FINGER_TOUCH_HALF_WIDTH, rect.centerY()); + + return performTwoPointerGesture(startPoint1, startPoint2, endPoint1, endPoint2, steps); + } + + /** + * Generates a two-pointer gesture with arbitrary starting and ending points. + * + * @param startPoint1 start point of pointer 1 + * @param startPoint2 start point of pointer 2 + * @param endPoint1 end point of pointer 1 + * @param endPoint2 end point of pointer 2 + * @param steps the number of steps for the gesture. Steps are injected + * about 5 milliseconds apart, so 100 steps may take around 0.5 seconds to complete. + * @return <code>true</code> if all touch events for this gesture are injected successfully, + * <code>false</code> otherwise + * @since API Level 18 + */ + public boolean performTwoPointerGesture(Point startPoint1, Point startPoint2, Point endPoint1, + Point endPoint2, int steps) { + + // avoid a divide by zero + if(steps == 0) + steps = 1; + + final float stepX1 = (endPoint1.x - startPoint1.x) / steps; + final float stepY1 = (endPoint1.y - startPoint1.y) / steps; + final float stepX2 = (endPoint2.x - startPoint2.x) / steps; + final float stepY2 = (endPoint2.y - startPoint2.y) / steps; + + int eventX1, eventY1, eventX2, eventY2; + eventX1 = startPoint1.x; + eventY1 = startPoint1.y; + eventX2 = startPoint2.x; + eventY2 = startPoint2.y; + + // allocate for steps plus first down and last up + PointerCoords[] points1 = new PointerCoords[steps + 2]; + PointerCoords[] points2 = new PointerCoords[steps + 2]; + + // Include the first and last touch downs in the arrays of steps + for (int i = 0; i < steps + 1; i++) { + PointerCoords p1 = new PointerCoords(); + p1.x = eventX1; + p1.y = eventY1; + p1.pressure = 1; + p1.size = 1; + points1[i] = p1; + + PointerCoords p2 = new PointerCoords(); + p2.x = eventX2; + p2.y = eventY2; + p2.pressure = 1; + p2.size = 1; + points2[i] = p2; + + eventX1 += stepX1; + eventY1 += stepY1; + eventX2 += stepX2; + eventY2 += stepY2; + } + + // ending pointers coordinates + PointerCoords p1 = new PointerCoords(); + p1.x = endPoint1.x; + p1.y = endPoint1.y; + p1.pressure = 1; + p1.size = 1; + points1[steps + 1] = p1; + + PointerCoords p2 = new PointerCoords(); + p2.x = endPoint2.x; + p2.y = endPoint2.y; + p2.pressure = 1; + p2.size = 1; + points2[steps + 1] = p2; + + return performMultiPointerGesture(points1, points2); + } + + /** + * Performs a multi-touch gesture. You must specify touch coordinates for + * at least 2 pointers. Each pointer must have all of its touch steps + * defined in an array of {@link PointerCoords}. You can use this method to + * specify complex gestures, like circles and irregular shapes, 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 represents the pointers' paths. Each {@link PointerCoords} + * array represents a different pointer. Each {@link PointerCoords} in an + * array element represents a touch point on a pointer's path. + * @return <code>true</code> if all touch events for this gesture are injected successfully, + * <code>false</code> otherwise + * @since API Level 18 + */ + public boolean performMultiPointerGesture(PointerCoords[] ...touches) { + return getInteractionController().performMultiPointerGesture(touches); + } +} diff --git a/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiObjectNotFoundException.java b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiObjectNotFoundException.java new file mode 100644 index 0000000..fc0891b --- /dev/null +++ b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiObjectNotFoundException.java @@ -0,0 +1,48 @@ +/* + * 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; + +/** + * Generated in test runs when a {@link UiSelector} selector could not be matched + * to any UI element displayed. + * @since API Level 16 + */ +public class UiObjectNotFoundException extends Exception { + + private static final long serialVersionUID = 1L; + + /** + * @since API Level 16 + **/ + public UiObjectNotFoundException(String msg) { + super(msg); + } + + /** + * @since API Level 16 + **/ + public UiObjectNotFoundException(String detailMessage, Throwable throwable) { + super(detailMessage, throwable); + } + + /** + * @since API Level 16 + **/ + public UiObjectNotFoundException(Throwable throwable) { + super(throwable); + } +} diff --git a/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiScrollable.java b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiScrollable.java new file mode 100644 index 0000000..a8d20c3 --- /dev/null +++ b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiScrollable.java @@ -0,0 +1,665 @@ +/* + * 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.graphics.Rect; +import android.util.Log; +import android.view.accessibility.AccessibilityNodeInfo; + +/** + * UiScrollable is a {@link UiCollection} and provides support for searching + * for items in scrollable layout elements. This class can be used with + * horizontally or vertically scrollable controls. + * @since API Level 16 + */ +public class UiScrollable extends UiCollection { + private static final String LOG_TAG = UiScrollable.class.getSimpleName(); + + // More steps slows the swipe and prevents contents from being flung too far + private static final int SCROLL_STEPS = 55; + + private static final int FLING_STEPS = 5; + + // Restrict a swipe's starting and ending points inside a 10% margin of the target + private static final double DEFAULT_SWIPE_DEADZONE_PCT = 0.1; + + // Limits the number of swipes/scrolls performed during a search + private static int mMaxSearchSwipes = 30; + + // Used in ScrollForward() and ScrollBackward() to determine swipe direction + private boolean mIsVerticalList = true; + + private double mSwipeDeadZonePercentage = DEFAULT_SWIPE_DEADZONE_PCT; + + /** + * Constructor. + * + * @param container a {@link UiSelector} selector to identify the scrollable + * layout element. + * @since API Level 16 + */ + public UiScrollable(UiSelector container) { + // wrap the container selector with container so that QueryController can handle + // this type of enumeration search accordingly + super(container); + } + + /** + * Set the direction of swipes to be vertical when performing scroll actions. + * @return reference to itself + * @since API Level 16 + */ + public UiScrollable setAsVerticalList() { + Tracer.trace(); + mIsVerticalList = true; + return this; + } + + /** + * Set the direction of swipes to be horizontal when performing scroll actions. + * @return reference to itself + * @since API Level 16 + */ + public UiScrollable setAsHorizontalList() { + Tracer.trace(); + mIsVerticalList = false; + return this; + } + + /** + * Used privately when performing swipe searches to decide if an element has become + * visible or not. + * + * @param selector + * @return true if found else false + * @since API Level 16 + */ + protected boolean exists(UiSelector selector) { + if(getQueryController().findAccessibilityNodeInfo(selector) != null) { + return true; + } + return false; + } + + /** + * Searches for a child element in the present scrollable container. + * The search first looks for a child element that matches the selector + * you provided, then looks for the content-description in its children elements. + * If both search conditions are fulfilled, the method returns a {@ link UiObject} + * representing the element matching the selector (not the child element in its + * subhierarchy containing the content-description). By default, this method performs a + * scroll search. + * See {@link #getChildByDescription(UiSelector, String, boolean)} + * + * @param childPattern {@link UiSelector} for a child in a scollable layout element + * @param text Content-description to find in the children of + * the <code>childPattern</code> match + * @return {@link UiObject} representing the child element that matches the search conditions + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + @Override + public UiObject getChildByDescription(UiSelector childPattern, String text) + throws UiObjectNotFoundException { + Tracer.trace(childPattern, text); + return getChildByDescription(childPattern, text, true); + } + + /** + * Searches for a child element in the present scrollable container. + * The search first looks for a child element that matches the selector + * you provided, then looks for the content-description in its children elements. + * If both search conditions are fulfilled, the method returns a {@ link UiObject} + * representing the element matching the selector (not the child element in its + * subhierarchy containing the content-description). + * + * @param childPattern {@link UiSelector} for a child in a scollable layout element + * @param text Content-description to find in the children of + * the <code>childPattern</code> match (may be a partial match) + * @param allowScrollSearch set to true if scrolling is allowed + * @return {@link UiObject} representing the child element that matches the search conditions + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public UiObject getChildByDescription(UiSelector childPattern, String text, + boolean allowScrollSearch) throws UiObjectNotFoundException { + Tracer.trace(childPattern, text, allowScrollSearch); + if (text != null) { + if (allowScrollSearch) { + scrollIntoView(new UiSelector().descriptionContains(text)); + } + return super.getChildByDescription(childPattern, text); + } + throw new UiObjectNotFoundException("for description= \"" + text + "\""); + } + + /** + * Searches for a child element in the present scrollable container that + * matches the selector you provided. The search is performed without + * scrolling and only on visible elements. + * + * @param childPattern {@link UiSelector} for a child in a scollable layout element + * @param instance int number representing the occurance of + * a <code>childPattern</code> match + * @return {@link UiObject} representing the child element that matches the search conditions + * @since API Level 16 + */ + @Override + public UiObject getChildByInstance(UiSelector childPattern, int instance) + throws UiObjectNotFoundException { + Tracer.trace(childPattern, instance); + UiSelector patternSelector = UiSelector.patternBuilder(getSelector(), + UiSelector.patternBuilder(childPattern).instance(instance)); + return new UiObject(patternSelector); + } + + /** + * Searches for a child element in the present scrollable + * container. The search first looks for a child element that matches the + * selector you provided, then looks for the text in its children elements. + * If both search conditions are fulfilled, the method returns a {@ link UiObject} + * representing the element matching the selector (not the child element in its + * subhierarchy containing the text). By default, this method performs a + * scroll search. + * See {@link #getChildByText(UiSelector, String, boolean)} + * + * @param childPattern {@link UiSelector} selector for a child in a scrollable layout element + * @param text String to find in the children of the <code>childPattern</code> match + * @return {@link UiObject} representing the child element that matches the search conditions + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + @Override + public UiObject getChildByText(UiSelector childPattern, String text) + throws UiObjectNotFoundException { + Tracer.trace(childPattern, text); + return getChildByText(childPattern, text, true); + } + + /** + * Searches for a child element in the present scrollable container. The + * search first looks for a child element that matches the + * selector you provided, then looks for the text in its children elements. + * If both search conditions are fulfilled, the method returns a {@ link UiObject} + * representing the element matching the selector (not the child element in its + * subhierarchy containing the text). + * + * @param childPattern {@link UiSelector} selector for a child in a scrollable layout element + * @param text String to find in the children of the <code>childPattern</code> match + * @param allowScrollSearch set to true if scrolling is allowed + * @return {@link UiObject} representing the child element that matches the search conditions + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public UiObject getChildByText(UiSelector childPattern, String text, boolean allowScrollSearch) + throws UiObjectNotFoundException { + Tracer.trace(childPattern, text, allowScrollSearch); + if (text != null) { + if (allowScrollSearch) { + scrollIntoView(new UiSelector().text(text)); + } + return super.getChildByText(childPattern, text); + } + throw new UiObjectNotFoundException("for text= \"" + text + "\""); + } + + /** + * Performs a forward scroll action on the scrollable layout element until + * the content-description is found, or until swipe attempts have been exhausted. + * See {@link #setMaxSearchSwipes(int)} + * + * @param text content-description to find within the contents of this scrollable layout element. + * @return true if item is found; else, false + * @since API Level 16 + */ + public boolean scrollDescriptionIntoView(String text) throws UiObjectNotFoundException { + Tracer.trace(text); + return scrollIntoView(new UiSelector().description(text)); + } + + /** + * Perform a forward scroll action to move through the scrollable layout element until + * a visible item that matches the {@link UiObject} is found. + * + * @param obj {@link UiObject} + * @return true if the item was found and now is in view else false + * @since API Level 16 + */ + public boolean scrollIntoView(UiObject obj) throws UiObjectNotFoundException { + Tracer.trace(obj.getSelector()); + return scrollIntoView(obj.getSelector()); + } + + /** + * Perform a scroll forward action to move through the scrollable layout + * element until a visible item that matches the selector is found. + * + * See {@link #scrollDescriptionIntoView(String)} and {@link #scrollTextIntoView(String)}. + * + * @param selector {@link UiSelector} selector + * @return true if the item was found and now is in view; else, false + * @since API Level 16 + */ + public boolean scrollIntoView(UiSelector selector) throws UiObjectNotFoundException { + Tracer.trace(selector); + // if we happen to be on top of the text we want then return here + UiSelector childSelector = getSelector().childSelector(selector); + if (exists(childSelector)) { + return (true); + } else { + // we will need to reset the search from the beginning to start search + scrollToBeginning(mMaxSearchSwipes); + if (exists(childSelector)) { + return (true); + } + for (int x = 0; x < mMaxSearchSwipes; x++) { + boolean scrolled = scrollForward(); + if(exists(childSelector)) { + return true; + } + if (!scrolled) { + return false; + } + } + } + return false; + } + + /** + * Scrolls forward until the UiObject is fully visible in the scrollable container. + * Use this method to make sure that the child item's edges are not offscreen. + * + * @param childObject {@link UiObject} representing the child element + * @return true if the child element is already fully visible, or + * if the method scrolled successfully until the child became fully visible; + * otherwise, false if the attempt to scroll failed. + * @throws UiObjectNotFoundException + * @hide + */ + public boolean ensureFullyVisible(UiObject childObject) throws UiObjectNotFoundException { + Rect actual = childObject.getBounds(); + Rect visible = childObject.getVisibleBounds(); + if (visible.width() * visible.height() == actual.width() * actual.height()) { + // area match, item fully visible + return true; + } + boolean shouldSwipeForward = false; + if (mIsVerticalList) { + // if list is vertical, matching top edge implies obscured bottom edge + // so we need to scroll list forward + shouldSwipeForward = actual.top == visible.top; + } else { + // if list is horizontal, matching left edge implies obscured right edge, + // so we need to scroll list forward + shouldSwipeForward = actual.left == visible.left; + } + if (mIsVerticalList) { + if (shouldSwipeForward) { + return swipeUp(10); + } else { + return swipeDown(10); + } + } else { + if (shouldSwipeForward) { + return swipeLeft(10); + } else { + return swipeRight(10); + } + } + } + + /** + * Performs a forward scroll action on the scrollable layout element until + * the text you provided is visible, or until swipe attempts have been exhausted. + * See {@link #setMaxSearchSwipes(int)} + * + * @param text test to look for + * @return true if item is found; else, false + * @since API Level 16 + */ + public boolean scrollTextIntoView(String text) throws UiObjectNotFoundException { + Tracer.trace(text); + return scrollIntoView(new UiSelector().text(text)); + } + + /** + * Sets the maximum number of scrolls allowed when performing a + * scroll action in search of a child element. + * See {@link #getChildByDescription(UiSelector, String)} and + * {@link #getChildByText(UiSelector, String)}. + * + * @param swipes the number of search swipes to perform until giving up + * @return reference to itself + * @since API Level 16 + */ + public UiScrollable setMaxSearchSwipes(int swipes) { + Tracer.trace(swipes); + mMaxSearchSwipes = swipes; + return this; + } + + /** + * Gets the maximum number of scrolls allowed when performing a + * scroll action in search of a child element. + * See {@link #getChildByDescription(UiSelector, String)} and + * {@link #getChildByText(UiSelector, String)}. + * + * @return max the number of search swipes to perform until giving up + * @since API Level 16 + */ + public int getMaxSearchSwipes() { + Tracer.trace(); + return mMaxSearchSwipes; + } + + /** + * Performs a forward fling with the default number of fling steps (5). + * If the swipe direction is set to vertical, then the swipes will be + * performed from bottom to top. If the swipe + * direction is set to horizontal, then the swipes will be performed from + * right to left. Make sure to take into account devices configured with + * right-to-left languages like Arabic and Hebrew. + * + * @return true if scrolled, false if can't scroll anymore + * @since API Level 16 + */ + public boolean flingForward() throws UiObjectNotFoundException { + Tracer.trace(); + return scrollForward(FLING_STEPS); + } + + /** + * Performs a forward scroll with the default number of scroll steps (55). + * If the swipe direction is set to vertical, + * then the swipes will be performed from bottom to top. If the swipe + * direction is set to horizontal, then the swipes will be performed from + * right to left. Make sure to take into account devices configured with + * right-to-left languages like Arabic and Hebrew. + * + * @return true if scrolled, false if can't scroll anymore + * @since API Level 16 + */ + public boolean scrollForward() throws UiObjectNotFoundException { + Tracer.trace(); + return scrollForward(SCROLL_STEPS); + } + + /** + * Performs a forward scroll. If the swipe direction is set to vertical, + * then the swipes will be performed from bottom to top. If the swipe + * direction is set to horizontal, then the swipes will be performed from + * right to left. Make sure to take into account devices configured with + * right-to-left languages like Arabic and Hebrew. + * + * @param steps number of steps. Use this to control the speed of the scroll action + * @return true if scrolled, false if can't scroll anymore + * @since API Level 16 + */ + public boolean scrollForward(int steps) throws UiObjectNotFoundException { + Tracer.trace(steps); + Log.d(LOG_TAG, "scrollForward() on selector = " + getSelector()); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + Rect rect = new Rect(); + node.getBoundsInScreen(rect); + + int downX = 0; + int downY = 0; + int upX = 0; + int upY = 0; + + // scrolling is by default assumed vertically unless the object is explicitly + // set otherwise by setAsHorizontalContainer() + if(mIsVerticalList) { + int swipeAreaAdjust = (int)(rect.height() * getSwipeDeadZonePercentage()); + // scroll vertically: swipe down -> up + downX = rect.centerX(); + downY = rect.bottom - swipeAreaAdjust; + upX = rect.centerX(); + upY = rect.top + swipeAreaAdjust; + } else { + int swipeAreaAdjust = (int)(rect.width() * getSwipeDeadZonePercentage()); + // scroll horizontally: swipe right -> left + // TODO: Assuming device is not in right to left language + downX = rect.right - swipeAreaAdjust; + downY = rect.centerY(); + upX = rect.left + swipeAreaAdjust; + upY = rect.centerY(); + } + return getInteractionController().scrollSwipe(downX, downY, upX, upY, steps); + } + + /** + * Performs a backwards fling action with the default number of fling + * steps (5). If the swipe direction is set to vertical, + * then the swipe will be performed from top to bottom. If the swipe + * direction is set to horizontal, then the swipes will be performed from + * left to right. Make sure to take into account devices configured with + * right-to-left languages like Arabic and Hebrew. + * + * @return true if scrolled, and false if can't scroll anymore + * @since API Level 16 + */ + public boolean flingBackward() throws UiObjectNotFoundException { + Tracer.trace(); + return scrollBackward(FLING_STEPS); + } + + /** + * Performs a backward scroll with the default number of scroll steps (55). + * If the swipe direction is set to vertical, + * then the swipes will be performed from top to bottom. If the swipe + * direction is set to horizontal, then the swipes will be performed from + * left to right. Make sure to take into account devices configured with + * right-to-left languages like Arabic and Hebrew. + * + * @return true if scrolled, and false if can't scroll anymore + * @since API Level 16 + */ + public boolean scrollBackward() throws UiObjectNotFoundException { + Tracer.trace(); + return scrollBackward(SCROLL_STEPS); + } + + /** + * Performs a backward scroll. If the swipe direction is set to vertical, + * then the swipes will be performed from top to bottom. If the swipe + * direction is set to horizontal, then the swipes will be performed from + * left to right. Make sure to take into account devices configured with + * right-to-left languages like Arabic and Hebrew. + * + * @param steps number of steps. Use this to control the speed of the scroll action. + * @return true if scrolled, false if can't scroll anymore + * @since API Level 16 + */ + public boolean scrollBackward(int steps) throws UiObjectNotFoundException { + Tracer.trace(steps); + Log.d(LOG_TAG, "scrollBackward() on selector = " + getSelector()); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT); + if (node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + Rect rect = new Rect(); + node.getBoundsInScreen(rect); + + int downX = 0; + int downY = 0; + int upX = 0; + int upY = 0; + + // scrolling is by default assumed vertically unless the object is explicitly + // set otherwise by setAsHorizontalContainer() + if(mIsVerticalList) { + int swipeAreaAdjust = (int)(rect.height() * getSwipeDeadZonePercentage()); + Log.d(LOG_TAG, "scrollToBegining() using vertical scroll"); + // scroll vertically: swipe up -> down + downX = rect.centerX(); + downY = rect.top + swipeAreaAdjust; + upX = rect.centerX(); + upY = rect.bottom - swipeAreaAdjust; + } else { + int swipeAreaAdjust = (int)(rect.width() * getSwipeDeadZonePercentage()); + Log.d(LOG_TAG, "scrollToBegining() using hotizontal scroll"); + // scroll horizontally: swipe left -> right + // TODO: Assuming device is not in right to left language + downX = rect.left + swipeAreaAdjust; + downY = rect.centerY(); + upX = rect.right - swipeAreaAdjust; + upY = rect.centerY(); + } + return getInteractionController().scrollSwipe(downX, downY, upX, upY, steps); + } + + /** + * Scrolls to the beginning of a scrollable layout element. The beginning + * can be at the top-most edge in the case of vertical controls, or the + * left-most edge for horizontal controls. Make sure to take into account + * devices configured with right-to-left languages like Arabic and Hebrew. + * + * @param steps use steps to control the speed, so that it may be a scroll, or fling + * @return true on scrolled else false + * @since API Level 16 + */ + public boolean scrollToBeginning(int maxSwipes, int steps) throws UiObjectNotFoundException { + Tracer.trace(maxSwipes, steps); + Log.d(LOG_TAG, "scrollToBeginning() on selector = " + getSelector()); + // protect against potential hanging and return after preset attempts + for(int x = 0; x < maxSwipes; x++) { + if(!scrollBackward(steps)) { + break; + } + } + return true; + } + + /** + * Scrolls to the beginning of a scrollable layout element. The beginning + * can be at the top-most edge in the case of vertical controls, or the + * left-most edge for horizontal controls. Make sure to take into account + * devices configured with right-to-left languages like Arabic and Hebrew. + * + * @param maxSwipes + * @return true on scrolled else false + * @since API Level 16 + */ + public boolean scrollToBeginning(int maxSwipes) throws UiObjectNotFoundException { + Tracer.trace(maxSwipes); + return scrollToBeginning(maxSwipes, SCROLL_STEPS); + } + + /** + * Performs a fling gesture to reach the beginning of a scrollable layout element. + * The beginning can be at the top-most edge in the case of vertical controls, or + * the left-most edge for horizontal controls. Make sure to take into + * account devices configured with right-to-left languages like Arabic and Hebrew. + * + * @param maxSwipes + * @return true on scrolled else false + * @since API Level 16 + */ + public boolean flingToBeginning(int maxSwipes) throws UiObjectNotFoundException { + Tracer.trace(maxSwipes); + return scrollToBeginning(maxSwipes, FLING_STEPS); + } + + /** + * Scrolls to the end of a scrollable layout element. The end can be at the + * bottom-most edge in the case of vertical controls, or the right-most edge for + * horizontal controls. Make sure to take into account devices configured with + * right-to-left languages like Arabic and Hebrew. + * + * @param steps use steps to control the speed, so that it may be a scroll, or fling + * @return true on scrolled else false + * @since API Level 16 + */ + public boolean scrollToEnd(int maxSwipes, int steps) throws UiObjectNotFoundException { + Tracer.trace(maxSwipes, steps); + // protect against potential hanging and return after preset attempts + for(int x = 0; x < maxSwipes; x++) { + if(!scrollForward(steps)) { + break; + } + } + return true; + } + + /** + * Scrolls to the end of a scrollable layout element. The end can be at the + * bottom-most edge in the case of vertical controls, or the right-most edge for + * horizontal controls. Make sure to take into account devices configured with + * right-to-left languages like Arabic and Hebrew. + * + * @param maxSwipes + * @return true on scrolled, else false + * @since API Level 16 + */ + public boolean scrollToEnd(int maxSwipes) throws UiObjectNotFoundException { + Tracer.trace(maxSwipes); + return scrollToEnd(maxSwipes, SCROLL_STEPS); + } + + /** + * Performs a fling gesture to reach the end of a scrollable layout element. + * The end can be at the bottom-most edge in the case of vertical controls, or + * the right-most edge for horizontal controls. Make sure to take into + * account devices configured with right-to-left languages like Arabic and Hebrew. + * + * @param maxSwipes + * @return true on scrolled, else false + * @since API Level 16 + */ + public boolean flingToEnd(int maxSwipes) throws UiObjectNotFoundException { + Tracer.trace(maxSwipes); + return scrollToEnd(maxSwipes, FLING_STEPS); + } + + /** + * Returns the percentage of a widget's size that's considered as a no-touch + * zone when swiping. The no-touch zone is set as a percentage of a widget's total + * width or height, denoting a margin around the swipable area of the widget. + * Swipes must start and end inside this margin. This is important when the + * widget being swiped may not respond to the swipe if started at a point + * too near to the edge. The default is 10% from either edge. + * + * @return a value between 0 and 1 + * @since API Level 16 + */ + public double getSwipeDeadZonePercentage() { + Tracer.trace(); + return mSwipeDeadZonePercentage; + } + + /** + * Sets the percentage of a widget's size that's considered as no-touch + * zone when swiping. + * The no-touch zone is set as percentage of a widget's total width or height, + * denoting a margin around the swipable area of the widget. Swipes must + * always start and end inside this margin. This is important when the + * widget being swiped may not respond to the swipe if started at a point + * too near to the edge. The default is 10% from either edge. + * + * @param swipeDeadZonePercentage is a value between 0 and 1 + * @return reference to itself + * @since API Level 16 + */ + public UiScrollable setSwipeDeadZonePercentage(double swipeDeadZonePercentage) { + Tracer.trace(swipeDeadZonePercentage); + mSwipeDeadZonePercentage = swipeDeadZonePercentage; + return this; + } +} diff --git a/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiSelector.java b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiSelector.java new file mode 100644 index 0000000..482a74d --- /dev/null +++ b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiSelector.java @@ -0,0 +1,1022 @@ +/* + * 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.util.SparseArray; +import android.view.accessibility.AccessibilityNodeInfo; + +import java.util.regex.Pattern; + +/** + * Specifies the elements in the layout hierarchy for tests to target, filtered + * by properties such as text value, content-description, class name, and state + * information. You can also target an element by its location in a layout + * hierarchy. + * @since API Level 16 + */ +public class UiSelector { + static final int SELECTOR_NIL = 0; + static final int SELECTOR_TEXT = 1; + static final int SELECTOR_START_TEXT = 2; + static final int SELECTOR_CONTAINS_TEXT = 3; + static final int SELECTOR_CLASS = 4; + static final int SELECTOR_DESCRIPTION = 5; + static final int SELECTOR_START_DESCRIPTION = 6; + static final int SELECTOR_CONTAINS_DESCRIPTION = 7; + static final int SELECTOR_INDEX = 8; + static final int SELECTOR_INSTANCE = 9; + static final int SELECTOR_ENABLED = 10; + static final int SELECTOR_FOCUSED = 11; + static final int SELECTOR_FOCUSABLE = 12; + static final int SELECTOR_SCROLLABLE = 13; + static final int SELECTOR_CLICKABLE = 14; + static final int SELECTOR_CHECKED = 15; + static final int SELECTOR_SELECTED = 16; + static final int SELECTOR_ID = 17; + static final int SELECTOR_PACKAGE_NAME = 18; + static final int SELECTOR_CHILD = 19; + static final int SELECTOR_CONTAINER = 20; + static final int SELECTOR_PATTERN = 21; + static final int SELECTOR_PARENT = 22; + static final int SELECTOR_COUNT = 23; + static final int SELECTOR_LONG_CLICKABLE = 24; + static final int SELECTOR_TEXT_REGEX = 25; + static final int SELECTOR_CLASS_REGEX = 26; + static final int SELECTOR_DESCRIPTION_REGEX = 27; + static final int SELECTOR_PACKAGE_NAME_REGEX = 28; + static final int SELECTOR_RESOURCE_ID = 29; + static final int SELECTOR_CHECKABLE = 30; + static final int SELECTOR_RESOURCE_ID_REGEX = 31; + + private SparseArray<Object> mSelectorAttributes = new SparseArray<Object>(); + + /** + * @since API Level 16 + */ + public UiSelector() { + } + + UiSelector(UiSelector selector) { + mSelectorAttributes = selector.cloneSelector().mSelectorAttributes; + } + + /** + * @since API Level 17 + */ + protected UiSelector cloneSelector() { + UiSelector ret = new UiSelector(); + ret.mSelectorAttributes = mSelectorAttributes.clone(); + if (hasChildSelector()) + ret.mSelectorAttributes.put(SELECTOR_CHILD, new UiSelector(getChildSelector())); + if (hasParentSelector()) + ret.mSelectorAttributes.put(SELECTOR_PARENT, new UiSelector(getParentSelector())); + if (hasPatternSelector()) + ret.mSelectorAttributes.put(SELECTOR_PATTERN, new UiSelector(getPatternSelector())); + return ret; + } + + static UiSelector patternBuilder(UiSelector selector) { + if (!selector.hasPatternSelector()) { + return new UiSelector().patternSelector(selector); + } + return selector; + } + + static UiSelector patternBuilder(UiSelector container, UiSelector pattern) { + return new UiSelector( + new UiSelector().containerSelector(container).patternSelector(pattern)); + } + + /** + * Set the search criteria to match the visible text displayed + * in a widget (for example, the text label to launch an app). + * + * The text for the element must match exactly with the string in your input + * argument. Matching is case-sensitive. + * + * @param text Value to match + * @return UiSelector with the specified search criteria + * @since API Level 16 + */ + public UiSelector text(String text) { + return buildSelector(SELECTOR_TEXT, text); + } + + /** + * Set the search criteria to match the visible text displayed in a layout + * element, using a regular expression. + * + * The text in the widget must match exactly with the string in your + * input argument. + * + * @param regex a regular expression + * @return UiSelector with the specified search criteria + * @since API Level 17 + */ + public UiSelector textMatches(String regex) { + return buildSelector(SELECTOR_TEXT_REGEX, Pattern.compile(regex)); + } + + /** + * Set the search criteria to match visible text in a widget that is + * prefixed by the text parameter. + * + * The matching is case-insensitive. + * + * @param text Value to match + * @return UiSelector with the specified search criteria + * @since API Level 16 + */ + public UiSelector textStartsWith(String text) { + return buildSelector(SELECTOR_START_TEXT, text); + } + + /** + * Set the search criteria to match the visible text in a widget + * where the visible text must contain the string in your input argument. + * + * The matching is case-sensitive. + * + * @param text Value to match + * @return UiSelector with the specified search criteria + * @since API Level 16 + */ + public UiSelector textContains(String text) { + return buildSelector(SELECTOR_CONTAINS_TEXT, text); + } + + /** + * Set the search criteria to match the class property + * for a widget (for example, "android.widget.Button"). + * + * @param className Value to match + * @return UiSelector with the specified search criteria + * @since API Level 16 + */ + public UiSelector className(String className) { + return buildSelector(SELECTOR_CLASS, className); + } + + /** + * Set the search criteria to match the class property + * for a widget, using a regular expression. + * + * @param regex a regular expression + * @return UiSelector with the specified search criteria + * @since API Level 17 + */ + public UiSelector classNameMatches(String regex) { + return buildSelector(SELECTOR_CLASS_REGEX, Pattern.compile(regex)); + } + + /** + * Set the search criteria to match the class property + * for a widget (for example, "android.widget.Button"). + * + * @param type type + * @return UiSelector with the specified search criteria + * @since API Level 17 + */ + public <T> UiSelector className(Class<T> type) { + return buildSelector(SELECTOR_CLASS, type.getName()); + } + + /** + * Set the search criteria to match the content-description + * property for a widget. + * + * The content-description is typically used + * by the Android Accessibility framework to + * provide an audio prompt for the widget when + * the widget is selected. The content-description + * for the widget must match exactly + * with the string in your input argument. + * + * Matching is case-sensitive. + * + * @param desc Value to match + * @return UiSelector with the specified search criteria + * @since API Level 16 + */ + public UiSelector description(String desc) { + return buildSelector(SELECTOR_DESCRIPTION, desc); + } + + /** + * Set the search criteria to match the content-description + * property for a widget. + * + * The content-description is typically used + * by the Android Accessibility framework to + * provide an audio prompt for the widget when + * the widget is selected. The content-description + * for the widget must match exactly + * with the string in your input argument. + * + * @param regex a regular expression + * @return UiSelector with the specified search criteria + * @since API Level 17 + */ + public UiSelector descriptionMatches(String regex) { + return buildSelector(SELECTOR_DESCRIPTION_REGEX, Pattern.compile(regex)); + } + + /** + * Set the search criteria to match the content-description + * property for a widget. + * + * The content-description is typically used + * by the Android Accessibility framework to + * provide an audio prompt for the widget when + * the widget is selected. The content-description + * for the widget must start + * with the string in your input argument. + * + * Matching is case-insensitive. + * + * @param desc Value to match + * @return UiSelector with the specified search criteria + * @since API Level 16 + */ + public UiSelector descriptionStartsWith(String desc) { + return buildSelector(SELECTOR_START_DESCRIPTION, desc); + } + + /** + * Set the search criteria to match the content-description + * property for a widget. + * + * The content-description is typically used + * by the Android Accessibility framework to + * provide an audio prompt for the widget when + * the widget is selected. The content-description + * for the widget must contain + * the string in your input argument. + * + * Matching is case-insensitive. + * + * @param desc Value to match + * @return UiSelector with the specified search criteria + * @since API Level 16 + */ + public UiSelector descriptionContains(String desc) { + return buildSelector(SELECTOR_CONTAINS_DESCRIPTION, desc); + } + + /** + * Set the search criteria to match the given resource ID. + * + * @param id Value to match + * @return UiSelector with the specified search criteria + * @since API Level 18 + */ + public UiSelector resourceId(String id) { + return buildSelector(SELECTOR_RESOURCE_ID, id); + } + + /** + * Set the search criteria to match the resource ID + * of the widget, using a regular expression. + * + * @param regex a regular expression + * @return UiSelector with the specified search criteria + * @since API Level 18 + */ + public UiSelector resourceIdMatches(String regex) { + return buildSelector(SELECTOR_RESOURCE_ID_REGEX, Pattern.compile(regex)); + } + + /** + * Set the search criteria to match the widget by its node + * index in the layout hierarchy. + * + * The index value must be 0 or greater. + * + * Using the index can be unreliable and should only + * be used as a last resort for matching. Instead, + * consider using the {@link #instance(int)} method. + * + * @param index Value to match + * @return UiSelector with the specified search criteria + * @since API Level 16 + */ + public UiSelector index(final int index) { + return buildSelector(SELECTOR_INDEX, index); + } + + /** + * Set the search criteria to match the + * widget by its instance number. + * + * The instance value must be 0 or greater, where + * the first instance is 0. + * + * For example, to simulate a user click on + * the third image that is enabled in a UI screen, you + * could specify a a search criteria where the instance is + * 2, the {@link #className(String)} matches the image + * widget class, and {@link #enabled(boolean)} is true. + * The code would look like this: + * <code> + * new UiSelector().className("android.widget.ImageView") + * .enabled(true).instance(2); + * </code> + * + * @param instance Value to match + * @return UiSelector with the specified search criteria + * @since API Level 16 + */ + public UiSelector instance(final int instance) { + return buildSelector(SELECTOR_INSTANCE, instance); + } + + /** + * Set the search criteria to match widgets that are enabled. + * + * Typically, using this search criteria alone is not useful. + * You should also include additional criteria, such as text, + * content-description, or the class name for a widget. + * + * If no other search criteria is specified, and there is more + * than one matching widget, the first widget in the tree + * is selected. + * + * @param val Value to match + * @return UiSelector with the specified search criteria + * @since API Level 16 + */ + public UiSelector enabled(boolean val) { + return buildSelector(SELECTOR_ENABLED, val); + } + + /** + * Set the search criteria to match widgets that have focus. + * + * Typically, using this search criteria alone is not useful. + * You should also include additional criteria, such as text, + * content-description, or the class name for a widget. + * + * If no other search criteria is specified, and there is more + * than one matching widget, the first widget in the tree + * is selected. + * + * @param val Value to match + * @return UiSelector with the specified search criteria + * @since API Level 16 + */ + public UiSelector focused(boolean val) { + return buildSelector(SELECTOR_FOCUSED, val); + } + + /** + * Set the search criteria to match widgets that are focusable. + * + * Typically, using this search criteria alone is not useful. + * You should also include additional criteria, such as text, + * content-description, or the class name for a widget. + * + * If no other search criteria is specified, and there is more + * than one matching widget, the first widget in the tree + * is selected. + * + * @param val Value to match + * @return UiSelector with the specified search criteria + * @since API Level 16 + */ + public UiSelector focusable(boolean val) { + return buildSelector(SELECTOR_FOCUSABLE, val); + } + + /** + * Set the search criteria to match widgets that are scrollable. + * + * Typically, using this search criteria alone is not useful. + * You should also include additional criteria, such as text, + * content-description, or the class name for a widget. + * + * If no other search criteria is specified, and there is more + * than one matching widget, the first widget in the tree + * is selected. + * + * @param val Value to match + * @return UiSelector with the specified search criteria + * @since API Level 16 + */ + public UiSelector scrollable(boolean val) { + return buildSelector(SELECTOR_SCROLLABLE, val); + } + + /** + * Set the search criteria to match widgets that + * are currently selected. + * + * Typically, using this search criteria alone is not useful. + * You should also include additional criteria, such as text, + * content-description, or the class name for a widget. + * + * If no other search criteria is specified, and there is more + * than one matching widget, the first widget in the tree + * is selected. + * + * @param val Value to match + * @return UiSelector with the specified search criteria + * @since API Level 16 + */ + public UiSelector selected(boolean val) { + return buildSelector(SELECTOR_SELECTED, val); + } + + /** + * Set the search criteria to match widgets that + * are currently checked (usually for checkboxes). + * + * Typically, using this search criteria alone is not useful. + * You should also include additional criteria, such as text, + * content-description, or the class name for a widget. + * + * If no other search criteria is specified, and there is more + * than one matching widget, the first widget in the tree + * is selected. + * + * @param val Value to match + * @return UiSelector with the specified search criteria + * @since API Level 16 + */ + public UiSelector checked(boolean val) { + return buildSelector(SELECTOR_CHECKED, val); + } + + /** + * Set the search criteria to match widgets that are clickable. + * + * Typically, using this search criteria alone is not useful. + * You should also include additional criteria, such as text, + * content-description, or the class name for a widget. + * + * If no other search criteria is specified, and there is more + * than one matching widget, the first widget in the tree + * is selected. + * + * @param val Value to match + * @return UiSelector with the specified search criteria + * @since API Level 16 + */ + public UiSelector clickable(boolean val) { + return buildSelector(SELECTOR_CLICKABLE, val); + } + + /** + * Set the search criteria to match widgets that are checkable. + * + * Typically, using this search criteria alone is not useful. + * You should also include additional criteria, such as text, + * content-description, or the class name for a widget. + * + * If no other search criteria is specified, and there is more + * than one matching widget, the first widget in the tree + * is selected. + * + * @param val Value to match + * @return UiSelector with the specified search criteria + * @since API Level 18 + */ + public UiSelector checkable(boolean val) { + return buildSelector(SELECTOR_CHECKABLE, val); + } + + /** + * Set the search criteria to match widgets that are long-clickable. + * + * Typically, using this search criteria alone is not useful. + * You should also include additional criteria, such as text, + * content-description, or the class name for a widget. + * + * If no other search criteria is specified, and there is more + * than one matching widget, the first widget in the tree + * is selected. + * + * @param val Value to match + * @return UiSelector with the specified search criteria + * @since API Level 17 + */ + public UiSelector longClickable(boolean val) { + return buildSelector(SELECTOR_LONG_CLICKABLE, val); + } + + /** + * Adds a child UiSelector criteria to this selector. + * + * Use this selector to narrow the search scope to + * child widgets under a specific parent widget. + * + * @param selector + * @return UiSelector with this added search criterion + * @since API Level 16 + */ + public UiSelector childSelector(UiSelector selector) { + return buildSelector(SELECTOR_CHILD, selector); + } + + private UiSelector patternSelector(UiSelector selector) { + return buildSelector(SELECTOR_PATTERN, selector); + } + + private UiSelector containerSelector(UiSelector selector) { + return buildSelector(SELECTOR_CONTAINER, selector); + } + + /** + * Adds a child UiSelector criteria to this selector which is used to + * start search from the parent widget. + * + * Use this selector to narrow the search scope to + * sibling widgets as well all child widgets under a parent. + * + * @param selector + * @return UiSelector with this added search criterion + * @since API Level 16 + */ + public UiSelector fromParent(UiSelector selector) { + return buildSelector(SELECTOR_PARENT, selector); + } + + /** + * Set the search criteria to match the package name + * of the application that contains the widget. + * + * @param name Value to match + * @return UiSelector with the specified search criteria + * @since API Level 16 + */ + public UiSelector packageName(String name) { + return buildSelector(SELECTOR_PACKAGE_NAME, name); + } + + /** + * Set the search criteria to match the package name + * of the application that contains the widget. + * + * @param regex a regular expression + * @return UiSelector with the specified search criteria + * @since API Level 17 + */ + public UiSelector packageNameMatches(String regex) { + return buildSelector(SELECTOR_PACKAGE_NAME_REGEX, Pattern.compile(regex)); + } + + /** + * Building a UiSelector always returns a new UiSelector and never modifies the + * existing UiSelector being used. + */ + private UiSelector buildSelector(int selectorId, Object selectorValue) { + UiSelector selector = new UiSelector(this); + if (selectorId == SELECTOR_CHILD || selectorId == SELECTOR_PARENT) + selector.getLastSubSelector().mSelectorAttributes.put(selectorId, selectorValue); + else + selector.mSelectorAttributes.put(selectorId, selectorValue); + return selector; + } + + /** + * Selectors may have a hierarchy defined by specifying child nodes to be matched. + * It is not necessary that every selector have more than one level. A selector + * can also be a single level referencing only one node. In such cases the return + * it null. + * + * @return a child selector if one exists. Else null if this selector does not + * reference child node. + */ + UiSelector getChildSelector() { + UiSelector selector = (UiSelector)mSelectorAttributes.get(UiSelector.SELECTOR_CHILD, null); + if (selector != null) + return new UiSelector(selector); + return null; + } + + UiSelector getPatternSelector() { + UiSelector selector = + (UiSelector)mSelectorAttributes.get(UiSelector.SELECTOR_PATTERN, null); + if (selector != null) + return new UiSelector(selector); + return null; + } + + UiSelector getContainerSelector() { + UiSelector selector = + (UiSelector)mSelectorAttributes.get(UiSelector.SELECTOR_CONTAINER, null); + if (selector != null) + return new UiSelector(selector); + return null; + } + + UiSelector getParentSelector() { + UiSelector selector = + (UiSelector) mSelectorAttributes.get(UiSelector.SELECTOR_PARENT, null); + if (selector != null) + return new UiSelector(selector); + return null; + } + + int getInstance() { + return getInt(UiSelector.SELECTOR_INSTANCE); + } + + String getString(int criterion) { + return (String) mSelectorAttributes.get(criterion, null); + } + + boolean getBoolean(int criterion) { + return (Boolean) mSelectorAttributes.get(criterion, false); + } + + int getInt(int criterion) { + return (Integer) mSelectorAttributes.get(criterion, 0); + } + + Pattern getPattern(int criterion) { + return (Pattern) mSelectorAttributes.get(criterion, null); + } + + boolean isMatchFor(AccessibilityNodeInfo node, int index) { + int size = mSelectorAttributes.size(); + for(int x = 0; x < size; x++) { + CharSequence s = null; + int criterion = mSelectorAttributes.keyAt(x); + switch(criterion) { + case UiSelector.SELECTOR_INDEX: + if (index != this.getInt(criterion)) + return false; + break; + case UiSelector.SELECTOR_CHECKED: + if (node.isChecked() != getBoolean(criterion)) { + return false; + } + break; + case UiSelector.SELECTOR_CLASS: + s = node.getClassName(); + if (s == null || !s.toString().contentEquals(getString(criterion))) { + return false; + } + break; + case UiSelector.SELECTOR_CLASS_REGEX: + s = node.getClassName(); + if (s == null || !getPattern(criterion).matcher(s).matches()) { + return false; + } + break; + case UiSelector.SELECTOR_CLICKABLE: + if (node.isClickable() != getBoolean(criterion)) { + return false; + } + break; + case UiSelector.SELECTOR_CHECKABLE: + if (node.isCheckable() != getBoolean(criterion)) { + return false; + } + break; + case UiSelector.SELECTOR_LONG_CLICKABLE: + if (node.isLongClickable() != getBoolean(criterion)) { + return false; + } + break; + case UiSelector.SELECTOR_CONTAINS_DESCRIPTION: + s = node.getContentDescription(); + if (s == null || !s.toString().toLowerCase() + .contains(getString(criterion).toLowerCase())) { + return false; + } + break; + case UiSelector.SELECTOR_START_DESCRIPTION: + s = node.getContentDescription(); + if (s == null || !s.toString().toLowerCase() + .startsWith(getString(criterion).toLowerCase())) { + return false; + } + break; + case UiSelector.SELECTOR_DESCRIPTION: + s = node.getContentDescription(); + if (s == null || !s.toString().contentEquals(getString(criterion))) { + return false; + } + break; + case UiSelector.SELECTOR_DESCRIPTION_REGEX: + s = node.getContentDescription(); + if (s == null || !getPattern(criterion).matcher(s).matches()) { + return false; + } + break; + case UiSelector.SELECTOR_CONTAINS_TEXT: + s = node.getText(); + if (s == null || !s.toString().toLowerCase() + .contains(getString(criterion).toLowerCase())) { + return false; + } + break; + case UiSelector.SELECTOR_START_TEXT: + s = node.getText(); + if (s == null || !s.toString().toLowerCase() + .startsWith(getString(criterion).toLowerCase())) { + return false; + } + break; + case UiSelector.SELECTOR_TEXT: + s = node.getText(); + if (s == null || !s.toString().contentEquals(getString(criterion))) { + return false; + } + break; + case UiSelector.SELECTOR_TEXT_REGEX: + s = node.getText(); + if (s == null || !getPattern(criterion).matcher(s).matches()) { + return false; + } + break; + case UiSelector.SELECTOR_ENABLED: + if (node.isEnabled() != getBoolean(criterion)) { + return false; + } + break; + case UiSelector.SELECTOR_FOCUSABLE: + if (node.isFocusable() != getBoolean(criterion)) { + return false; + } + break; + case UiSelector.SELECTOR_FOCUSED: + if (node.isFocused() != getBoolean(criterion)) { + return false; + } + break; + case UiSelector.SELECTOR_ID: + break; //TODO: do we need this for AccessibilityNodeInfo.id? + case UiSelector.SELECTOR_PACKAGE_NAME: + s = node.getPackageName(); + if (s == null || !s.toString().contentEquals(getString(criterion))) { + return false; + } + break; + case UiSelector.SELECTOR_PACKAGE_NAME_REGEX: + s = node.getPackageName(); + if (s == null || !getPattern(criterion).matcher(s).matches()) { + return false; + } + break; + case UiSelector.SELECTOR_SCROLLABLE: + if (node.isScrollable() != getBoolean(criterion)) { + return false; + } + break; + case UiSelector.SELECTOR_SELECTED: + if (node.isSelected() != getBoolean(criterion)) { + return false; + } + break; + case UiSelector.SELECTOR_RESOURCE_ID: + s = node.getViewIdResourceName(); + if (s == null || !s.toString().contentEquals(getString(criterion))) { + return false; + } + break; + case UiSelector.SELECTOR_RESOURCE_ID_REGEX: + s = node.getViewIdResourceName(); + if (s == null || !getPattern(criterion).matcher(s).matches()) { + return false; + } + break; + } + } + return matchOrUpdateInstance(); + } + + private boolean matchOrUpdateInstance() { + int currentSelectorCounter = 0; + int currentSelectorInstance = 0; + + // matched attributes - now check for matching instance number + if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_INSTANCE) >= 0) { + currentSelectorInstance = + (Integer)mSelectorAttributes.get(UiSelector.SELECTOR_INSTANCE); + } + + // instance is required. Add count if not already counting + if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_COUNT) >= 0) { + currentSelectorCounter = (Integer)mSelectorAttributes.get(UiSelector.SELECTOR_COUNT); + } + + // Verify + if (currentSelectorInstance == currentSelectorCounter) { + return true; + } + // Update count + if (currentSelectorInstance > currentSelectorCounter) { + mSelectorAttributes.put(UiSelector.SELECTOR_COUNT, ++currentSelectorCounter); + } + return false; + } + + /** + * Leaf selector indicates no more child or parent selectors + * are declared in the this selector. + * @return true if is leaf. + */ + boolean isLeaf() { + if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_CHILD) < 0 && + mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_PARENT) < 0) { + return true; + } + return false; + } + + boolean hasChildSelector() { + if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_CHILD) < 0) { + return false; + } + return true; + } + + boolean hasPatternSelector() { + if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_PATTERN) < 0) { + return false; + } + return true; + } + + boolean hasContainerSelector() { + if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_CONTAINER) < 0) { + return false; + } + return true; + } + + boolean hasParentSelector() { + if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_PARENT) < 0) { + return false; + } + return true; + } + + /** + * Returns the deepest selector in the chain of possible sub selectors. + * A chain of selector is created when either of {@link UiSelector#childSelector(UiSelector)} + * or {@link UiSelector#fromParent(UiSelector)} are used once or more in the construction of + * a selector. + * @return last UiSelector in chain + */ + private UiSelector getLastSubSelector() { + if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_CHILD) >= 0) { + UiSelector child = (UiSelector)mSelectorAttributes.get(UiSelector.SELECTOR_CHILD); + if (child.getLastSubSelector() == null) { + return child; + } + return child.getLastSubSelector(); + } else if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_PARENT) >= 0) { + UiSelector parent = (UiSelector)mSelectorAttributes.get(UiSelector.SELECTOR_PARENT); + if (parent.getLastSubSelector() == null) { + return parent; + } + return parent.getLastSubSelector(); + } + return this; + } + + @Override + public String toString() { + return dumpToString(true); + } + + String dumpToString(boolean all) { + StringBuilder builder = new StringBuilder(); + builder.append(UiSelector.class.getSimpleName() + "["); + final int criterionCount = mSelectorAttributes.size(); + for (int i = 0; i < criterionCount; i++) { + if (i > 0) { + builder.append(", "); + } + final int criterion = mSelectorAttributes.keyAt(i); + switch (criterion) { + case SELECTOR_TEXT: + builder.append("TEXT=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_TEXT_REGEX: + builder.append("TEXT_REGEX=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_START_TEXT: + builder.append("START_TEXT=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_CONTAINS_TEXT: + builder.append("CONTAINS_TEXT=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_CLASS: + builder.append("CLASS=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_CLASS_REGEX: + builder.append("CLASS_REGEX=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_DESCRIPTION: + builder.append("DESCRIPTION=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_DESCRIPTION_REGEX: + builder.append("DESCRIPTION_REGEX=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_START_DESCRIPTION: + builder.append("START_DESCRIPTION=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_CONTAINS_DESCRIPTION: + builder.append("CONTAINS_DESCRIPTION=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_INDEX: + builder.append("INDEX=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_INSTANCE: + builder.append("INSTANCE=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_ENABLED: + builder.append("ENABLED=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_FOCUSED: + builder.append("FOCUSED=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_FOCUSABLE: + builder.append("FOCUSABLE=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_SCROLLABLE: + builder.append("SCROLLABLE=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_CLICKABLE: + builder.append("CLICKABLE=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_CHECKABLE: + builder.append("CHECKABLE=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_LONG_CLICKABLE: + builder.append("LONG_CLICKABLE=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_CHECKED: + builder.append("CHECKED=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_SELECTED: + builder.append("SELECTED=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_ID: + builder.append("ID=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_CHILD: + if (all) + builder.append("CHILD=").append(mSelectorAttributes.valueAt(i)); + else + builder.append("CHILD[..]"); + break; + case SELECTOR_PATTERN: + if (all) + builder.append("PATTERN=").append(mSelectorAttributes.valueAt(i)); + else + builder.append("PATTERN[..]"); + break; + case SELECTOR_CONTAINER: + if (all) + builder.append("CONTAINER=").append(mSelectorAttributes.valueAt(i)); + else + builder.append("CONTAINER[..]"); + break; + case SELECTOR_PARENT: + if (all) + builder.append("PARENT=").append(mSelectorAttributes.valueAt(i)); + else + builder.append("PARENT[..]"); + break; + case SELECTOR_COUNT: + builder.append("COUNT=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_PACKAGE_NAME: + builder.append("PACKAGE NAME=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_PACKAGE_NAME_REGEX: + builder.append("PACKAGE_NAME_REGEX=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_RESOURCE_ID: + builder.append("RESOURCE_ID=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_RESOURCE_ID_REGEX: + builder.append("RESOURCE_ID_REGEX=").append(mSelectorAttributes.valueAt(i)); + break; + default: + builder.append("UNDEFINED="+criterion+" ").append(mSelectorAttributes.valueAt(i)); + } + } + builder.append("]"); + return builder.toString(); + } +} diff --git a/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiWatcher.java b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiWatcher.java new file mode 100644 index 0000000..5403e30 --- /dev/null +++ b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiWatcher.java @@ -0,0 +1,48 @@ +/* + * 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; + +/** + * See {@link UiDevice#registerWatcher(String, UiWatcher)} on how to register a + * a condition watcher to be called by the automation library. The automation library will + * invoke checkForCondition() only when a regular API call is in retry mode because it is unable + * to locate its selector yet. Only during this time, the watchers are invoked to check if there is + * something else unexpected on the screen. + * @since API Level 16 + */ +public interface UiWatcher { + + /** + * Custom handler that is automatically called when the testing framework is unable to + * find a match using the {@link UiSelector} + * + * When the framework is in the process of matching a {@link UiSelector} and it + * is unable to match any widget based on the specified criteria in the selector, + * the framework will perform retries for a predetermined time, waiting for the display + * to update and show the desired widget. While the framework is in this state, it will call + * registered watchers' checkForCondition(). This gives the registered watchers a chance + * to take a look at the display and see if there is a recognized condition that can be + * handled and in doing so allowing the current test to continue. + * + * An example usage would be to look for dialogs popped due to other background + * processes requesting user attention and have nothing to do with the application + * currently under test. + * + * @return true to indicate a matched condition or false for nothing was matched + * @since API Level 16 + */ + public boolean checkForCondition(); +} |