QueryController.java revision e54d649fb83a0a44516e5c25a9ac1992c8950e59
1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package com.android.uiautomator.core;
17
18import android.os.SystemClock;
19import android.util.Log;
20import android.view.accessibility.AccessibilityEvent;
21import android.view.accessibility.AccessibilityNodeInfo;
22
23import com.android.uiautomator.core.UiAutomatorBridge.AccessibilityEventListener;
24
25/**
26 * The QuertController main purpose is to translate a {@link By} selectors to
27 * {@link AccessibilityNodeInfo}. This is all this controller does. It is typically
28 * created in conjunction with a {@link InteractionController} by {@link UiAutomationContext}
29 * which owns both. {@link UiAutomationContext} is used by {@link UiBase} classes.
30 */
31class QueryController {
32
33    private static final String LOG_TAG = QueryController.class.getSimpleName();
34
35    private static final boolean DEBUG = false;
36
37    private final UiAutomatorBridge mUiAutomatorBridge;
38
39    private final Object mLock = new Object();
40
41    private String mLastActivityName = null;
42    private String mLastPackageName = null;
43
44    // During a pattern selector search, the recursive pattern search
45    // methods will track their counts and indexes here.
46    private int mPatternCounter = 0;
47    private int mPatternIndexer = 0;
48
49    // These help show each selector's search context as it relates to the previous sub selector
50    // matched. When a compound selector fails, it is hard to tell which part of it is failing.
51    // Seeing how a selector is being parsed and which sub selector failed within a long list
52    // of compound selectors is very helpful.
53    private int mLogIndent = 0;
54    private int mLogParentIndent = 0;
55
56    private String mLastTraversedText = "";
57
58    public QueryController(UiAutomatorBridge bridge) {
59        mUiAutomatorBridge = bridge;
60        bridge.addAccessibilityEventListener(new AccessibilityEventListener() {
61            @Override
62            public void onAccessibilityEvent(AccessibilityEvent event) {
63                synchronized (mLock) {
64                    mLastPackageName = event.getPackageName().toString();
65                    // don't trust event.getText(), check for nulls
66                    if (event.getText() != null && event.getText().size() > 0) {
67                        if(event.getText().get(0) != null)
68                            mLastActivityName = event.getText().get(0).toString();
69                    }
70
71                    switch(event.getEventType()) {
72                    case AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY:
73                        // don't trust event.getText(), check for nulls
74                        if (event.getText() != null && event.getText().size() > 0)
75                            if(event.getText().get(0) != null)
76                                mLastTraversedText = event.getText().get(0).toString();
77                        if(DEBUG)
78                            Log.i(LOG_TAG, "Last text selection reported: " + mLastTraversedText);
79                        break;
80                    }
81                    mLock.notifyAll();
82                }
83            }
84        });
85    }
86
87    /**
88     * Returns the last text selection reported by accessibility
89     * event TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY. One way to cause
90     * this event is using a DPad arrows to focus on UI elements.
91     * @return
92     */
93    public String getLastTraversedText() {
94        mUiAutomatorBridge.waitForIdle();
95        synchronized (mLock) {
96            if (mLastTraversedText.length() > 0) {
97                return mLastTraversedText;
98            }
99        }
100        return null;
101    }
102
103    /**
104     * Clears the last text selection value saved from the TYPE_VIEW_TEXT_SELECTION_CHANGED
105     * event
106     */
107    public void clearLastTraversedText() {
108        mUiAutomatorBridge.waitForIdle();
109        synchronized (mLock) {
110            mLastTraversedText = "";
111        }
112    }
113
114    private void initializeNewSearch() {
115        mPatternCounter = 0;
116        mPatternIndexer = 0;
117        mLogIndent = 0;
118        mLogParentIndent = 0;;
119    }
120
121    /**
122     * Counts the instances of the selector group. The selector must be in the following
123     * format: [container_selector, PATTERN=[INSTANCE=x, PATTERN=[the_pattern]]
124     * where the container_selector is used to find the containment region to search for patterns
125     * and the INSTANCE=x is the instance of the_pattern to return.
126     * @param selector
127     * @return number of pattern matches. Returns 0 for all other cases.
128     */
129    public int getPatternCount(By selector) {
130        findAccessibilityNodeInfo(selector, true /*counting*/);
131        return mPatternCounter;
132    }
133
134    /**
135     * Main search method for translating By selectors to AccessibilityInfoNodes
136     * @param selector
137     * @return
138     */
139    public AccessibilityNodeInfo findAccessibilityNodeInfo(By selector) {
140        return findAccessibilityNodeInfo(selector, false);
141    }
142
143    protected AccessibilityNodeInfo findAccessibilityNodeInfo(By selector, boolean isCounting) {
144        mUiAutomatorBridge.waitForIdle();
145        initializeNewSearch();
146
147        if(DEBUG)
148            Log.i(LOG_TAG, "Searching: " + selector);
149
150        synchronized (mLock) {
151            AccessibilityNodeInfo rootNode = getRootNode();
152            if (rootNode == null) {
153                Log.e(LOG_TAG, "Cannot proceed when root node is null. Aborted search");
154                return null;
155            }
156
157            // Copy so that we don't modify the original's sub selectors
158            By bySelector = By.selector(selector);
159            return translateCompoundSelector(bySelector, rootNode, isCounting);
160        }
161    }
162
163    /**
164     * Gets the root node from accessibility and if it fails to get one it will
165     * retry every 250ms for up to 1000ms.
166     * @return null if no root node is obtained
167     */
168    protected AccessibilityNodeInfo getRootNode() {
169        final int maxRetry = 4;
170        final long waitInterval = 250;
171        AccessibilityNodeInfo rootNode = null;
172        for(int x = 0; x < maxRetry; x++) {
173            rootNode = mUiAutomatorBridge.getRootAccessibilityNodeInfoInActiveWindow();
174            if (rootNode != null) {
175                return rootNode;
176            }
177            if(x < maxRetry - 1) {
178                Log.e(LOG_TAG, "Got null root node from accessibility - Retrying...");
179                SystemClock.sleep(waitInterval);
180            }
181        }
182        return rootNode;
183    }
184
185    /**
186     * A compoundSelector encapsulate both Regular and Pattern selectors. The formats follows:
187     * <p/>
188     * regular_selector = By[attributes... CHILD=By[attributes... CHILD=By[....]]]
189     * <br/>
190     * pattern_selector = ...CONTAINER=By[..] PATTERN=By[instance=x PATTERN=[regular_selector]
191     * <br/>
192     * compound_selector = [regular_selector [pattern_selector]]
193     * <p/>
194     * regular_selectors are the most common form of selectors and the search for them
195     * is straightforward. On the other hand pattern_selectors requires search to be
196     * performed as in regular_selector but where regular_selector search returns immediately
197     * upon a successful match, the search for pattern_selector continues until the
198     * requested matched _instance_ of that pattern is matched.
199     * <p/>
200     * Counting UI objects requires using pattern_selectors. The counting search is the same
201     * as a pattern_search however we're not looking to match an instance of the pattern but
202     * rather continuously walking the accessibility node hierarchy while counting matched
203     * patterns, until the end of the tree.
204     * <p/>
205     * If both present, order of parsing begins with CONTAINER followed by PATTERN then the
206     * top most selector is processed as regular_selector within the context of the previous
207     * CONTAINER and its PATTERN information. If neither is present then the top selector is
208     * directly treated as regular_selector. So the presence of a CONTAINER and PATTERN within
209     * a selector simply dictates that the selector matching will be constraint to the sub tree
210     * node where the CONTAINER and its child PATTERN have identified.
211     * @param bySelector
212     * @param fromNode
213     * @param isCounting
214     * @return
215     */
216    private AccessibilityNodeInfo translateCompoundSelector(By bySelector,
217            AccessibilityNodeInfo fromNode, boolean isCounting) {
218
219        // Start translating compound selectors by translating the regular_selector first
220        // The regular_selector is then used as a container for any optional pattern_selectors
221        // that may or may not be specified.
222        if(bySelector.hasContainerSelector())
223            // nested pattern selectors
224            if(bySelector.getContainerSelector().hasContainerSelector()) {
225                fromNode = translateCompoundSelector(
226                        bySelector.getContainerSelector(), fromNode, false);
227                initializeNewSearch();
228            } else
229                fromNode = translateReqularSelector(bySelector.getContainerSelector(), fromNode);
230        else
231            fromNode = translateReqularSelector(bySelector, fromNode);
232
233        if(fromNode == null) {
234            if(DEBUG)
235                Log.i(LOG_TAG, "Container selector not found: " + bySelector.dumpToString(false));
236            return null;
237        }
238
239        if(bySelector.hasPatternSelector()) {
240            fromNode = translatePatternSelector(bySelector.getPatternSelector(),
241                    fromNode, isCounting);
242
243            if (isCounting) {
244                Log.i(LOG_TAG, String.format(
245                        "Counted %d instances of: %s", mPatternCounter, bySelector));
246                return null;
247            } else {
248                if(fromNode == null) {
249                    if(DEBUG)
250                        Log.i(LOG_TAG, "Pattern selector not found: " +
251                                bySelector.dumpToString(false));
252                    return null;
253                }
254            }
255        }
256
257        // translate any additions to the selector that may have been added by tests
258        // with getChild(By selector) after a container and pattern selectors
259        if(bySelector.hasContainerSelector() || bySelector.hasPatternSelector()) {
260            if(bySelector.hasChildSelector() || bySelector.hasParentSelector())
261                fromNode = translateReqularSelector(bySelector, fromNode);
262        }
263
264        if(fromNode == null) {
265            if(DEBUG)
266                Log.i(LOG_TAG, "Object Not Found for selector " + bySelector);
267            return null;
268        }
269        Log.i(LOG_TAG, String.format("Matched selector: %s <<==>> [%s]", bySelector, fromNode));
270        return fromNode;
271    }
272
273    /**
274     * Used by the {@link #translateCompoundSelector(By, AccessibilityNodeInfo, boolean)}
275     * to translate the regular_selector portion. It has the following format:
276     * <p/>
277     * regular_selector = By[attributes... CHILD=By[attributes... CHILD=By[....]]]<br/>
278     * <p/>
279     * regular_selectors are the most common form of selectors and the search for them
280     * is straightforward. This method will only look for CHILD or PARENT sub selectors.
281     * <p/>
282     * @param selector
283     * @param fromNode
284     * @param index
285     * @return AccessibilityNodeInfo if found else null
286     */
287    private AccessibilityNodeInfo translateReqularSelector(By selector,
288            AccessibilityNodeInfo fromNode) {
289
290        return findNodeRegularRecursive(selector, fromNode, 0);
291    }
292
293    private AccessibilityNodeInfo findNodeRegularRecursive(By subSelector,
294            AccessibilityNodeInfo fromNode, int index) {
295
296        if (subSelector.isMatchFor(fromNode, index)) {
297            if (DEBUG) {
298                Log.d(LOG_TAG, formatLog(String.format("%s",
299                        subSelector.dumpToString(false))));
300            }
301            if(subSelector.isLeaf()) {
302                return fromNode;
303            }
304            if(subSelector.hasChildSelector()) {
305                mLogIndent++; // next selector
306                subSelector = subSelector.getChildSelector();
307                if(subSelector == null) {
308                    Log.e(LOG_TAG, "Error: A child selector without content");
309                    return null; // there is an implementation fault
310                }
311            } else if(subSelector.hasParentSelector()) {
312                mLogIndent++; // next selector
313                subSelector = subSelector.getParentSelector();
314                if(subSelector == null) {
315                    Log.e(LOG_TAG, "Error: A parent selector without content");
316                    return null; // there is an implementation fault
317                }
318                // the selector requested we start at this level from
319                // the parent node from the one we just matched
320                fromNode = fromNode.getParent();
321                if(fromNode == null)
322                    return null;
323            }
324        }
325
326        int childCount = fromNode.getChildCount();
327        boolean hasNullChild = false;
328        for (int i = 0; i < childCount; i++) {
329            AccessibilityNodeInfo childNode = fromNode.getChild(i);
330            if (childNode == null) {
331                Log.w(LOG_TAG, String.format(
332                        "AccessibilityNodeInfo returned a null child (%d of %d)", i, childCount));
333                if (!hasNullChild) {
334                    Log.w(LOG_TAG, String.format("parent = %s", fromNode.toString()));
335                }
336                hasNullChild = true;
337                continue;
338            }
339            if (!childNode.isVisibleToUser()) {
340                // TODO: need to remove this or move it under if (DEBUG)
341                Log.d(LOG_TAG, String.format("Skipping invisible child: %s", childNode.toString()));
342                continue;
343            }
344            AccessibilityNodeInfo retNode = findNodeRegularRecursive(subSelector, childNode, i);
345            if (retNode != null) {
346                return retNode;
347            }
348        }
349        return null;
350    }
351
352    /**
353     * Used by the {@link #translateCompoundSelector(By, AccessibilityNodeInfo, boolean)}
354     * to translate the pattern_selector portion. It has the following format:
355     * <p/>
356     * pattern_selector = ... PATTERN=By[instance=x PATTERN=[regular_selector]]<br/>
357     * <p/>
358     * pattern_selectors requires search to be performed as regular_selector but where
359     * regular_selector search returns immediately upon a successful match, the search for
360     * pattern_selector continues until the requested matched instance of that pattern is
361     * encountered.
362     * <p/>
363     * Counting UI objects requires using pattern_selectors. The counting search is the same
364     * as a pattern_search however we're not looking to match an instance of the pattern but
365     * rather continuously walking the accessibility node hierarchy while counting patterns
366     * until the end of the tree.
367     * @param subSelector
368     * @param fromNode
369     * @param originalPattern
370     * @return null of node is not found or if counting mode is true.
371     * See {@link #translateCompoundSelector(By, AccessibilityNodeInfo, boolean)}
372     */
373    private AccessibilityNodeInfo translatePatternSelector(By subSelector,
374            AccessibilityNodeInfo fromNode, boolean isCounting) {
375
376        if(subSelector.hasPatternSelector()) {
377            // Since pattern_selectors are also the type of selectors used when counting,
378            // we check if this is a counting run or an indexing run
379            if(isCounting)
380                //since we're counting, we reset the indexer so to terminates the search when
381                // the end of tree is reached. The count will be in mPatternCount
382                mPatternIndexer = -1;
383            else
384                // terminates the search once we match the pattern's instance
385                mPatternIndexer = subSelector.getInstance();
386
387            // A pattern is wrapped in a PATTERN[instance=x PATTERN[the_pattern]]
388            subSelector = subSelector.getPatternSelector();
389            if(subSelector == null) {
390                Log.e(LOG_TAG, "Pattern portion of the selector is null or not defined");
391                return null; // there is an implementation fault
392            }
393            // save the current indent level as parent indent before pattern searches
394            // begin under the current tree position.
395            mLogParentIndent = ++mLogIndent;
396            return findNodePatternRecursive(subSelector, fromNode, 0, subSelector);
397        }
398
399        Log.e(LOG_TAG, "Selector must have a pattern selector defined"); // implementation fault?
400        return null;
401    }
402
403    private AccessibilityNodeInfo findNodePatternRecursive(
404            By subSelector, AccessibilityNodeInfo fromNode, int index, By originalPattern) {
405
406        if (subSelector.isMatchFor(fromNode, index)) {
407            if(subSelector.isLeaf()) {
408                if(mPatternIndexer == 0) {
409                    if (DEBUG)
410                        Log.d(LOG_TAG, formatLog(
411                                String.format("%s", subSelector.dumpToString(false))));
412                    return fromNode;
413                } else {
414                    if(DEBUG)
415                        Log.d(LOG_TAG, formatLog(
416                                String.format("%s", subSelector.dumpToString(false))));
417                    mPatternCounter++; //count the pattern matched
418                    mPatternIndexer--; //decrement until zero for the instance requested
419
420                    // At a leaf selector within a group and still not instance matched
421                    // then reset the  selector to continue search from current position
422                    // in the accessibility tree for the next pattern match up until the
423                    // pattern index hits 0.
424                    subSelector = originalPattern;
425                    // starting over with next pattern search so reset to parent level
426                    mLogIndent = mLogParentIndent;
427                }
428            } else {
429                if(DEBUG)
430                    Log.d(LOG_TAG, formatLog(
431                            String.format("%s", subSelector.dumpToString(false))));
432
433                if(subSelector.hasChildSelector()) {
434                    mLogIndent++; // next selector
435                    subSelector = subSelector.getChildSelector();
436                    if(subSelector == null) {
437                        Log.e(LOG_TAG, "Error: A child selector without content");
438                        return null;
439                    }
440                } else if(subSelector.hasParentSelector()) {
441                    mLogIndent++; // next selector
442                    subSelector = subSelector.getParentSelector();
443                    if(subSelector == null) {
444                        Log.e(LOG_TAG, "Error: A parent selector without content");
445                        return null;
446                    }
447                    fromNode = fromNode.getParent();
448                    if(fromNode == null)
449                        return null;
450                }
451            }
452        }
453
454        int childCount = fromNode.getChildCount();
455        boolean hasNullChild = false;
456        for (int i = 0; i < childCount; i++) {
457            AccessibilityNodeInfo childNode = fromNode.getChild(i);
458            if (childNode == null) {
459                Log.w(LOG_TAG, String.format(
460                        "AccessibilityNodeInfo returned a null child (%d of %d)", i, childCount));
461                if (!hasNullChild) {
462                    Log.w(LOG_TAG, String.format("parent = %s", fromNode.toString()));
463                }
464                hasNullChild = true;
465                continue;
466            }
467            if (!childNode.isVisibleToUser()) {
468                // TODO: need to remove this or move it under if (DEBUG)
469                Log.d(LOG_TAG, String.format("Skipping invisible child: %s", childNode.toString()));
470                continue;
471            }
472            AccessibilityNodeInfo retNode = findNodePatternRecursive(
473                    subSelector, childNode, i, originalPattern);
474            if (retNode != null) {
475                return retNode;
476            }
477        }
478        return null;
479    }
480
481    public AccessibilityNodeInfo getAccessibilityRootNode() {
482        return mUiAutomatorBridge.getRootAccessibilityNodeInfoInActiveWindow();
483    }
484
485    /**
486     * Last activity to report accessibility events
487     * @return String name of activity
488     */
489    public String getCurrentActivityName() {
490        mUiAutomatorBridge.waitForIdle();
491        synchronized (mLock) {
492            return mLastActivityName;
493        }
494    }
495
496    /**
497     * Last package to report accessibility events
498     * @return String name of package
499     */
500    public String getCurrentPackageName() {
501        mUiAutomatorBridge.waitForIdle();
502        synchronized (mLock) {
503            return mLastPackageName;
504        }
505    }
506
507    private String formatLog(String str) {
508        StringBuilder l = new StringBuilder();
509        for(int space = 0; space < mLogIndent; space++)
510            l.append(". . ");
511        if(mLogIndent > 0)
512            l.append(String.format(". . [%d]: %s", mPatternCounter, str));
513        else
514            l.append(String.format(". . [%d]: %s", mPatternCounter, str));
515        return l.toString();
516    }
517}
518