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