summaryrefslogtreecommitdiffstats
path: root/cmds/uiautomator/library/core-src/com/android/uiautomator/core/QueryController.java
diff options
context:
space:
mode:
Diffstat (limited to 'cmds/uiautomator/library/core-src/com/android/uiautomator/core/QueryController.java')
-rw-r--r--cmds/uiautomator/library/core-src/com/android/uiautomator/core/QueryController.java521
1 files changed, 521 insertions, 0 deletions
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();
+ }
+}