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