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  */
16 
17 package androidx.test.uiautomator;
18 
19 import static java.util.Objects.requireNonNull;
20 
21 import android.util.SparseArray;
22 import android.view.accessibility.AccessibilityNodeInfo;
23 
24 import org.jspecify.annotations.NonNull;
25 
26 import java.util.regex.Pattern;
27 
28 /**
29  * Specifies the elements in the layout hierarchy for tests to target, filtered
30  * by properties such as text value, content-description, class name, and state
31  * information. You can also target an element by its location in a layout
32  * hierarchy.
33  */
34 public class UiSelector {
35     static final int SELECTOR_NIL = 0;
36     static final int SELECTOR_TEXT = 1;
37     static final int SELECTOR_START_TEXT = 2;
38     static final int SELECTOR_CONTAINS_TEXT = 3;
39     static final int SELECTOR_CLASS = 4;
40     static final int SELECTOR_DESCRIPTION = 5;
41     static final int SELECTOR_START_DESCRIPTION = 6;
42     static final int SELECTOR_CONTAINS_DESCRIPTION = 7;
43     static final int SELECTOR_INDEX = 8;
44     static final int SELECTOR_INSTANCE = 9;
45     static final int SELECTOR_ENABLED = 10;
46     static final int SELECTOR_FOCUSED = 11;
47     static final int SELECTOR_FOCUSABLE = 12;
48     static final int SELECTOR_SCROLLABLE = 13;
49     static final int SELECTOR_CLICKABLE = 14;
50     static final int SELECTOR_CHECKED = 15;
51     static final int SELECTOR_SELECTED = 16;
52     static final int SELECTOR_ID = 17;
53     static final int SELECTOR_PACKAGE_NAME = 18;
54     static final int SELECTOR_CHILD = 19;
55     static final int SELECTOR_CONTAINER = 20;
56     static final int SELECTOR_PATTERN = 21;
57     static final int SELECTOR_PARENT = 22;
58     static final int SELECTOR_COUNT = 23;
59     static final int SELECTOR_LONG_CLICKABLE = 24;
60     static final int SELECTOR_TEXT_REGEX = 25;
61     static final int SELECTOR_CLASS_REGEX = 26;
62     static final int SELECTOR_DESCRIPTION_REGEX = 27;
63     static final int SELECTOR_PACKAGE_NAME_REGEX = 28;
64     static final int SELECTOR_RESOURCE_ID = 29;
65     static final int SELECTOR_CHECKABLE = 30;
66     static final int SELECTOR_RESOURCE_ID_REGEX = 31;
67 
68     private SparseArray<Object> mSelectorAttributes = new SparseArray<>();
69 
UiSelector()70     public UiSelector() {
71     }
72 
UiSelector(UiSelector selector)73     UiSelector(UiSelector selector) {
74         mSelectorAttributes = selector.cloneSelector().mSelectorAttributes;
75     }
76 
cloneSelector()77     protected @NonNull UiSelector cloneSelector() {
78         UiSelector ret = new UiSelector();
79         ret.mSelectorAttributes = mSelectorAttributes.clone();
80         if (hasChildSelector())
81             ret.mSelectorAttributes.put(SELECTOR_CHILD, new UiSelector(getChildSelector()));
82         if (hasParentSelector())
83             ret.mSelectorAttributes.put(SELECTOR_PARENT, new UiSelector(getParentSelector()));
84         if (hasPatternSelector())
85             ret.mSelectorAttributes.put(SELECTOR_PATTERN, new UiSelector(getPatternSelector()));
86         return ret;
87     }
88 
patternBuilder(UiSelector selector)89     static UiSelector patternBuilder(UiSelector selector) {
90         if (!selector.hasPatternSelector()) {
91             return new UiSelector().patternSelector(selector);
92         }
93         return selector;
94     }
95 
patternBuilder(UiSelector container, UiSelector pattern)96     static UiSelector patternBuilder(UiSelector container, UiSelector pattern) {
97         return new UiSelector(
98                 new UiSelector().containerSelector(container).patternSelector(pattern));
99     }
100 
101     /**
102      * Set the search criteria to match the visible text displayed
103      * in a widget (for example, the text label to launch an app).
104      *
105      * The text for the element must match exactly with the string in your input
106      * argument. Matching is case-sensitive.
107      *
108      * @param text Value to match
109      * @return UiSelector with the specified search criteria
110      */
text(@onNull String text)111     public @NonNull UiSelector text(@NonNull String text) {
112         requireNonNull(text, "text cannot be null");
113         return buildSelector(SELECTOR_TEXT, text);
114     }
115 
116     /**
117      * Set the search criteria to match the visible text displayed in a layout
118      * element, using a regular expression.
119      *
120      * The text in the widget must match exactly with the string in your
121      * input argument.
122      *
123      * @param regex a regular expression
124      * @return UiSelector with the specified search criteria
125      */
textMatches(@onNull String regex)126     public @NonNull UiSelector textMatches(@NonNull String regex) {
127         requireNonNull(regex, "regex cannot be null");
128         return buildSelector(SELECTOR_TEXT_REGEX, Pattern.compile(regex, Pattern.DOTALL));
129     }
130 
131     /**
132      * Set the search criteria to match visible text in a widget that is
133      * prefixed by the text parameter.
134      *
135      * The matching is case-insensitive.
136      *
137      * @param text Value to match
138      * @return UiSelector with the specified search criteria
139      */
textStartsWith(@onNull String text)140     public @NonNull UiSelector textStartsWith(@NonNull String text) {
141         requireNonNull(text, "text cannot be null");
142         return buildSelector(SELECTOR_START_TEXT, text);
143     }
144 
145     /**
146      * Set the search criteria to match the visible text in a widget
147      * where the visible text must contain the string in your input argument.
148      *
149      * The matching is case-sensitive.
150      *
151      * @param text Value to match
152      * @return UiSelector with the specified search criteria
153      */
textContains(@onNull String text)154     public @NonNull UiSelector textContains(@NonNull String text) {
155         requireNonNull(text, "text cannot be null");
156         return buildSelector(SELECTOR_CONTAINS_TEXT, text);
157     }
158 
159     /**
160      * Set the search criteria to match the class property
161      * for a widget (for example, "android.widget.Button").
162      *
163      * @param className Value to match
164      * @return UiSelector with the specified search criteria
165      */
className(@onNull String className)166     public @NonNull UiSelector className(@NonNull String className) {
167         requireNonNull(className, "className cannot be null");
168         return buildSelector(SELECTOR_CLASS, className);
169     }
170 
171     /**
172      * Set the search criteria to match the class property
173      * for a widget, using a regular expression.
174      *
175      * @param regex a regular expression
176      * @return UiSelector with the specified search criteria
177      */
classNameMatches(@onNull String regex)178     public @NonNull UiSelector classNameMatches(@NonNull String regex) {
179         requireNonNull(regex, "regex cannot be null");
180         return buildSelector(SELECTOR_CLASS_REGEX, Pattern.compile(regex));
181     }
182 
183     /**
184      * Set the search criteria to match the class property
185      * for a widget (for example, "android.widget.Button").
186      *
187      * @param type type
188      * @return UiSelector with the specified search criteria
189      */
className(@onNull Class<T> type)190     public <T> @NonNull UiSelector className(@NonNull Class<T> type) {
191         requireNonNull(type, "type cannot be null");
192         return buildSelector(SELECTOR_CLASS, type.getName());
193     }
194 
195     /**
196      * Set the search criteria to match the content-description
197      * property for a widget.
198      *
199      * The content-description is typically used
200      * by the Android Accessibility framework to
201      * provide an audio prompt for the widget when
202      * the widget is selected. The content-description
203      * for the widget must match exactly
204      * with the string in your input argument.
205      *
206      * Matching is case-sensitive.
207      *
208      * @param desc Value to match
209      * @return UiSelector with the specified search criteria
210      */
description(@onNull String desc)211     public @NonNull UiSelector description(@NonNull String desc) {
212         requireNonNull(desc, "desc cannot be null");
213         return buildSelector(SELECTOR_DESCRIPTION, desc);
214     }
215 
216     /**
217      * Set the search criteria to match the content-description
218      * property for a widget.
219      *
220      * The content-description is typically used
221      * by the Android Accessibility framework to
222      * provide an audio prompt for the widget when
223      * the widget is selected. The content-description
224      * for the widget must match exactly
225      * with the string in your input argument.
226      *
227      * @param regex a regular expression
228      * @return UiSelector with the specified search criteria
229      */
descriptionMatches(@onNull String regex)230     public @NonNull UiSelector descriptionMatches(@NonNull String regex) {
231         requireNonNull(regex, "regex cannot be null");
232         return buildSelector(SELECTOR_DESCRIPTION_REGEX, Pattern.compile(regex, Pattern.DOTALL));
233     }
234 
235     /**
236      * Set the search criteria to match the content-description
237      * property for a widget.
238      *
239      * The content-description is typically used
240      * by the Android Accessibility framework to
241      * provide an audio prompt for the widget when
242      * the widget is selected. The content-description
243      * for the widget must start
244      * with the string in your input argument.
245      *
246      * Matching is case-insensitive.
247      *
248      * @param desc Value to match
249      * @return UiSelector with the specified search criteria
250      */
descriptionStartsWith(@onNull String desc)251     public @NonNull UiSelector descriptionStartsWith(@NonNull String desc) {
252         requireNonNull(desc, "desc cannot be null");
253         return buildSelector(SELECTOR_START_DESCRIPTION, desc);
254     }
255 
256     /**
257      * Set the search criteria to match the content-description
258      * property for a widget.
259      *
260      * The content-description is typically used
261      * by the Android Accessibility framework to
262      * provide an audio prompt for the widget when
263      * the widget is selected. The content-description
264      * for the widget must contain
265      * the string in your input argument.
266      *
267      * Matching is case-insensitive.
268      *
269      * @param desc Value to match
270      * @return UiSelector with the specified search criteria
271      */
descriptionContains(@onNull String desc)272     public @NonNull UiSelector descriptionContains(@NonNull String desc) {
273         requireNonNull(desc, "desc cannot be null");
274         return buildSelector(SELECTOR_CONTAINS_DESCRIPTION, desc);
275     }
276 
277     /**
278      * Set the search criteria to match the given resource ID.
279      *
280      * @param id Value to match
281      * @return UiSelector with the specified search criteria
282      */
resourceId(@onNull String id)283     public @NonNull UiSelector resourceId(@NonNull String id) {
284         requireNonNull(id, "id cannot be null");
285         return buildSelector(SELECTOR_RESOURCE_ID, id);
286     }
287 
288     /**
289      * Set the search criteria to match the resource ID
290      * of the widget, using a regular expression.
291      *
292      * @param regex a regular expression
293      * @return UiSelector with the specified search criteria
294      */
resourceIdMatches(@onNull String regex)295     public @NonNull UiSelector resourceIdMatches(@NonNull String regex) {
296         requireNonNull(regex, "regex cannot be null");
297         return buildSelector(SELECTOR_RESOURCE_ID_REGEX, Pattern.compile(regex));
298     }
299 
300     /**
301      * Set the search criteria to match the widget by its node
302      * index in the layout hierarchy.
303      *
304      * The index value must be 0 or greater.
305      *
306      * Using the index can be unreliable and should only
307      * be used as a last resort for matching. Instead,
308      * consider using the {@link #instance(int)} method.
309      *
310      * @param index Value to match
311      * @return UiSelector with the specified search criteria
312      */
index(final int index)313     public @NonNull UiSelector index(final int index) {
314         return buildSelector(SELECTOR_INDEX, index);
315     }
316 
317     /**
318      * Set the search criteria to match the
319      * widget by its instance number.
320      *
321      * The instance value must be 0 or greater, where
322      * the first instance is 0.
323      *
324      * For example, to simulate a user click on
325      * the third image that is enabled in a UI screen, you
326      * could specify a a search criteria where the instance is
327      * 2, the {@link #className(String)} matches the image
328      * widget class, and {@link #enabled(boolean)} is true.
329      * The code would look like this:
330      * <code>
331      * new UiSelector().className("android.widget.ImageView")
332      *    .enabled(true).instance(2);
333      * </code>
334      *
335      * @param instance Value to match
336      * @return UiSelector with the specified search criteria
337      */
instance(final int instance)338     public @NonNull UiSelector instance(final int instance) {
339         return buildSelector(SELECTOR_INSTANCE, instance);
340     }
341 
342     /**
343      * Set the search criteria to match widgets that are enabled.
344      *
345      * Typically, using this search criteria alone is not useful.
346      * You should also include additional criteria, such as text,
347      * content-description, or the class name for a widget.
348      *
349      * If no other search criteria is specified, and there is more
350      * than one matching widget, the first widget in the tree
351      * is selected.
352      *
353      * @param val Value to match
354      * @return UiSelector with the specified search criteria
355      */
enabled(boolean val)356     public @NonNull UiSelector enabled(boolean val) {
357         return buildSelector(SELECTOR_ENABLED, val);
358     }
359 
360     /**
361      * Set the search criteria to match widgets that have focus.
362      *
363      * Typically, using this search criteria alone is not useful.
364      * You should also include additional criteria, such as text,
365      * content-description, or the class name for a widget.
366      *
367      * If no other search criteria is specified, and there is more
368      * than one matching widget, the first widget in the tree
369      * is selected.
370      *
371      * @param val Value to match
372      * @return UiSelector with the specified search criteria
373      */
focused(boolean val)374     public @NonNull UiSelector focused(boolean val) {
375         return buildSelector(SELECTOR_FOCUSED, val);
376     }
377 
378     /**
379      * Set the search criteria to match widgets that are focusable.
380      *
381      * Typically, using this search criteria alone is not useful.
382      * You should also include additional criteria, such as text,
383      * content-description, or the class name for a widget.
384      *
385      * If no other search criteria is specified, and there is more
386      * than one matching widget, the first widget in the tree
387      * is selected.
388      *
389      * @param val Value to match
390      * @return UiSelector with the specified search criteria
391      */
focusable(boolean val)392     public @NonNull UiSelector focusable(boolean val) {
393         return buildSelector(SELECTOR_FOCUSABLE, val);
394     }
395 
396     /**
397      * Set the search criteria to match widgets that are scrollable.
398      *
399      * Typically, using this search criteria alone is not useful.
400      * You should also include additional criteria, such as text,
401      * content-description, or the class name for a widget.
402      *
403      * If no other search criteria is specified, and there is more
404      * than one matching widget, the first widget in the tree
405      * is selected.
406      *
407      * @param val Value to match
408      * @return UiSelector with the specified search criteria
409      */
scrollable(boolean val)410     public @NonNull UiSelector scrollable(boolean val) {
411         return buildSelector(SELECTOR_SCROLLABLE, val);
412     }
413 
414     /**
415      * Set the search criteria to match widgets that
416      * are currently selected.
417      *
418      * Typically, using this search criteria alone is not useful.
419      * You should also include additional criteria, such as text,
420      * content-description, or the class name for a widget.
421      *
422      * If no other search criteria is specified, and there is more
423      * than one matching widget, the first widget in the tree
424      * is selected.
425      *
426      * @param val Value to match
427      * @return UiSelector with the specified search criteria
428      */
selected(boolean val)429     public @NonNull UiSelector selected(boolean val) {
430         return buildSelector(SELECTOR_SELECTED, val);
431     }
432 
433     /**
434      * Set the search criteria to match widgets that
435      * are currently checked (usually for checkboxes).
436      *
437      * Typically, using this search criteria alone is not useful.
438      * You should also include additional criteria, such as text,
439      * content-description, or the class name for a widget.
440      *
441      * If no other search criteria is specified, and there is more
442      * than one matching widget, the first widget in the tree
443      * is selected.
444      *
445      * @param val Value to match
446      * @return UiSelector with the specified search criteria
447      */
checked(boolean val)448     public @NonNull UiSelector checked(boolean val) {
449         return buildSelector(SELECTOR_CHECKED, val);
450     }
451 
452     /**
453      * Set the search criteria to match widgets that are clickable.
454      *
455      * Typically, using this search criteria alone is not useful.
456      * You should also include additional criteria, such as text,
457      * content-description, or the class name for a widget.
458      *
459      * If no other search criteria is specified, and there is more
460      * than one matching widget, the first widget in the tree
461      * is selected.
462      *
463      * @param val Value to match
464      * @return UiSelector with the specified search criteria
465      */
clickable(boolean val)466     public @NonNull UiSelector clickable(boolean val) {
467         return buildSelector(SELECTOR_CLICKABLE, val);
468     }
469 
470     /**
471      * Set the search criteria to match widgets that are checkable.
472      *
473      * Typically, using this search criteria alone is not useful.
474      * You should also include additional criteria, such as text,
475      * content-description, or the class name for a widget.
476      *
477      * If no other search criteria is specified, and there is more
478      * than one matching widget, the first widget in the tree
479      * is selected.
480      *
481      * @param val Value to match
482      * @return UiSelector with the specified search criteria
483      */
checkable(boolean val)484     public @NonNull UiSelector checkable(boolean val) {
485         return buildSelector(SELECTOR_CHECKABLE, val);
486     }
487 
488     /**
489      * Set the search criteria to match widgets that are long-clickable.
490      *
491      * Typically, using this search criteria alone is not useful.
492      * You should also include additional criteria, such as text,
493      * content-description, or the class name for a widget.
494      *
495      * If no other search criteria is specified, and there is more
496      * than one matching widget, the first widget in the tree
497      * is selected.
498      *
499      * @param val Value to match
500      * @return UiSelector with the specified search criteria
501      */
longClickable(boolean val)502     public @NonNull UiSelector longClickable(boolean val) {
503         return buildSelector(SELECTOR_LONG_CLICKABLE, val);
504     }
505 
506     /**
507      * Adds a child UiSelector criteria to this selector.
508      *
509      * Use this selector to narrow the search scope to
510      * child widgets under a specific parent widget.
511      *
512      * @param selector
513      * @return UiSelector with this added search criterion
514      */
childSelector(@onNull UiSelector selector)515     public @NonNull UiSelector childSelector(@NonNull UiSelector selector) {
516         requireNonNull(selector, "selector cannot be null");
517         return buildSelector(SELECTOR_CHILD, selector);
518     }
519 
patternSelector(UiSelector selector)520     private UiSelector patternSelector(UiSelector selector) {
521         return buildSelector(SELECTOR_PATTERN, selector);
522     }
523 
containerSelector(UiSelector selector)524     private UiSelector containerSelector(UiSelector selector) {
525         return buildSelector(SELECTOR_CONTAINER, selector);
526     }
527 
528     /**
529      * Adds a child UiSelector criteria to this selector which is used to
530      * start search from the parent widget.
531      *
532      * Use this selector to narrow the search scope to
533      * sibling widgets as well all child widgets under a parent.
534      *
535      * @param selector
536      * @return UiSelector with this added search criterion
537      */
fromParent(@onNull UiSelector selector)538     public @NonNull UiSelector fromParent(@NonNull UiSelector selector) {
539         requireNonNull(selector, "selector cannot be null");
540         return buildSelector(SELECTOR_PARENT, selector);
541     }
542 
543     /**
544      * Set the search criteria to match the package name
545      * of the application that contains the widget.
546      *
547      * @param name Value to match
548      * @return UiSelector with the specified search criteria
549      */
packageName(@onNull String name)550     public @NonNull UiSelector packageName(@NonNull String name) {
551         requireNonNull(name, "name cannot be null");
552         return buildSelector(SELECTOR_PACKAGE_NAME, name);
553     }
554 
555     /**
556      * Set the search criteria to match the package name
557      * of the application that contains the widget.
558      *
559      * @param regex a regular expression
560      * @return UiSelector with the specified search criteria
561      */
packageNameMatches(@onNull String regex)562     public @NonNull UiSelector packageNameMatches(@NonNull String regex) {
563         requireNonNull(regex, "regex cannot be null");
564         return buildSelector(SELECTOR_PACKAGE_NAME_REGEX, Pattern.compile(regex));
565     }
566 
567     /**
568      * Building a UiSelector always returns a new UiSelector and never modifies the
569      * existing UiSelector being used.
570      */
buildSelector(int selectorId, Object selectorValue)571     private UiSelector buildSelector(int selectorId, Object selectorValue) {
572         UiSelector selector = new UiSelector(this);
573         if (selectorId == SELECTOR_CHILD || selectorId == SELECTOR_PARENT)
574             selector.getLastSubSelector().mSelectorAttributes.put(selectorId, selectorValue);
575         else
576             selector.mSelectorAttributes.put(selectorId, selectorValue);
577         return selector;
578     }
579 
580     /**
581      * Selectors may have a hierarchy defined by specifying child nodes to be matched.
582      * It is not necessary that every selector have more than one level. A selector
583      * can also be a single level referencing only one node. In such cases the return
584      * it null.
585      *
586      * @return a child selector if one exists. Else null if this selector does not
587      * reference child node.
588      */
getChildSelector()589     UiSelector getChildSelector() {
590         UiSelector selector = (UiSelector)mSelectorAttributes.get(UiSelector.SELECTOR_CHILD, null);
591         if (selector != null)
592             return new UiSelector(selector);
593         return null;
594     }
595 
getPatternSelector()596     UiSelector getPatternSelector() {
597         UiSelector selector =
598                 (UiSelector)mSelectorAttributes.get(UiSelector.SELECTOR_PATTERN, null);
599         if (selector != null)
600             return new UiSelector(selector);
601         return null;
602     }
603 
getContainerSelector()604     UiSelector getContainerSelector() {
605         UiSelector selector =
606                 (UiSelector)mSelectorAttributes.get(UiSelector.SELECTOR_CONTAINER, null);
607         if (selector != null)
608             return new UiSelector(selector);
609         return null;
610     }
611 
getParentSelector()612     UiSelector getParentSelector() {
613         UiSelector selector =
614                 (UiSelector) mSelectorAttributes.get(UiSelector.SELECTOR_PARENT, null);
615         if (selector != null)
616             return new UiSelector(selector);
617         return null;
618     }
619 
getInstance()620     int getInstance() {
621         return getInt(UiSelector.SELECTOR_INSTANCE);
622     }
623 
getString(int criterion)624     String getString(int criterion) {
625         return (String) mSelectorAttributes.get(criterion, null);
626     }
627 
getBoolean(int criterion)628     boolean getBoolean(int criterion) {
629         return (Boolean) mSelectorAttributes.get(criterion, false);
630     }
631 
getInt(int criterion)632     int getInt(int criterion) {
633         return (Integer) mSelectorAttributes.get(criterion, 0);
634     }
635 
getPattern(int criterion)636     Pattern getPattern(int criterion) {
637         return (Pattern) mSelectorAttributes.get(criterion, null);
638     }
639 
isMatchFor(AccessibilityNodeInfo node, int index)640     boolean isMatchFor(AccessibilityNodeInfo node, int index) {
641         int size = mSelectorAttributes.size();
642         for(int x = 0; x < size; x++) {
643             CharSequence s = null;
644             int criterion = mSelectorAttributes.keyAt(x);
645             switch(criterion) {
646             case UiSelector.SELECTOR_INDEX:
647                 if (index != this.getInt(criterion))
648                     return false;
649                 break;
650             case UiSelector.SELECTOR_CHECKED:
651                 if (node.isChecked() != getBoolean(criterion)) {
652                     return false;
653                 }
654                 break;
655             case UiSelector.SELECTOR_CLASS:
656                 s = node.getClassName();
657                 if (s == null || !s.toString().contentEquals(getString(criterion))) {
658                     return false;
659                 }
660                 break;
661             case UiSelector.SELECTOR_CLASS_REGEX:
662                 s = node.getClassName();
663                 if (s == null || !getPattern(criterion).matcher(s).matches()) {
664                     return false;
665                 }
666                 break;
667             case UiSelector.SELECTOR_CLICKABLE:
668                 if (node.isClickable() != getBoolean(criterion)) {
669                     return false;
670                 }
671                 break;
672             case UiSelector.SELECTOR_CHECKABLE:
673                 if (node.isCheckable() != getBoolean(criterion)) {
674                     return false;
675                 }
676                 break;
677             case UiSelector.SELECTOR_LONG_CLICKABLE:
678                 if (node.isLongClickable() != getBoolean(criterion)) {
679                     return false;
680                 }
681                 break;
682             case UiSelector.SELECTOR_CONTAINS_DESCRIPTION:
683                 s = node.getContentDescription();
684                 if (s == null || !s.toString().toLowerCase()
685                         .contains(getString(criterion).toLowerCase())) {
686                     return false;
687                 }
688                 break;
689             case UiSelector.SELECTOR_START_DESCRIPTION:
690                 s = node.getContentDescription();
691                 if (s == null || !s.toString().toLowerCase()
692                         .startsWith(getString(criterion).toLowerCase())) {
693                     return false;
694                 }
695                 break;
696             case UiSelector.SELECTOR_DESCRIPTION:
697                 s = node.getContentDescription();
698                 if (s == null || !s.toString().contentEquals(getString(criterion))) {
699                     return false;
700                 }
701                 break;
702             case UiSelector.SELECTOR_DESCRIPTION_REGEX:
703                 s = node.getContentDescription();
704                 if (s == null || !getPattern(criterion).matcher(s).matches()) {
705                     return false;
706                 }
707                 break;
708             case UiSelector.SELECTOR_CONTAINS_TEXT:
709                 s = node.getText();
710                 if (s == null || !s.toString().toLowerCase()
711                         .contains(getString(criterion).toLowerCase())) {
712                     return false;
713                 }
714                 break;
715             case UiSelector.SELECTOR_START_TEXT:
716                 s = node.getText();
717                 if (s == null || !s.toString().toLowerCase()
718                         .startsWith(getString(criterion).toLowerCase())) {
719                     return false;
720                 }
721                 break;
722             case UiSelector.SELECTOR_TEXT:
723                 s = node.getText();
724                 if (s == null || !s.toString().contentEquals(getString(criterion))) {
725                     return false;
726                 }
727                 break;
728             case UiSelector.SELECTOR_TEXT_REGEX:
729                 s = node.getText();
730                 if (s == null || !getPattern(criterion).matcher(s).matches()) {
731                     return false;
732                 }
733                 break;
734             case UiSelector.SELECTOR_ENABLED:
735                 if (node.isEnabled() != getBoolean(criterion)) {
736                     return false;
737                 }
738                 break;
739             case UiSelector.SELECTOR_FOCUSABLE:
740                 if (node.isFocusable() != getBoolean(criterion)) {
741                     return false;
742                 }
743                 break;
744             case UiSelector.SELECTOR_FOCUSED:
745                 if (node.isFocused() != getBoolean(criterion)) {
746                     return false;
747                 }
748                 break;
749             case UiSelector.SELECTOR_ID:
750                 break; //TODO: do we need this for AccessibilityNodeInfo.id?
751             case UiSelector.SELECTOR_PACKAGE_NAME:
752                 s = node.getPackageName();
753                 if (s == null || !s.toString().contentEquals(getString(criterion))) {
754                     return false;
755                 }
756                 break;
757             case UiSelector.SELECTOR_PACKAGE_NAME_REGEX:
758                 s = node.getPackageName();
759                 if (s == null || !getPattern(criterion).matcher(s).matches()) {
760                     return false;
761                 }
762                 break;
763             case UiSelector.SELECTOR_SCROLLABLE:
764                 if (node.isScrollable() != getBoolean(criterion)) {
765                     return false;
766                 }
767                 break;
768             case UiSelector.SELECTOR_SELECTED:
769                 if (node.isSelected() != getBoolean(criterion)) {
770                     return false;
771                 }
772                 break;
773             case UiSelector.SELECTOR_RESOURCE_ID:
774                 s = node.getViewIdResourceName();
775                 if (s == null || !s.toString().contentEquals(getString(criterion))) {
776                     return false;
777                 }
778                 break;
779             case UiSelector.SELECTOR_RESOURCE_ID_REGEX:
780                 s = node.getViewIdResourceName();
781                 if (s == null || !getPattern(criterion).matcher(s).matches()) {
782                     return false;
783                 }
784                 break;
785             }
786         }
787         return matchOrUpdateInstance();
788     }
789 
matchOrUpdateInstance()790     private boolean matchOrUpdateInstance() {
791         int currentSelectorCounter = 0;
792         int currentSelectorInstance = 0;
793 
794         // matched attributes - now check for matching instance number
795         if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_INSTANCE) >= 0) {
796             currentSelectorInstance =
797                     (Integer) mSelectorAttributes.get(UiSelector.SELECTOR_INSTANCE);
798         }
799 
800         // instance is required. Add count if not already counting
801         if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_COUNT) >= 0) {
802             currentSelectorCounter = (Integer) mSelectorAttributes.get(UiSelector.SELECTOR_COUNT);
803         }
804 
805         // Verify
806         if (currentSelectorInstance == currentSelectorCounter) {
807             return true;
808         }
809         // Update count
810         if (currentSelectorInstance > currentSelectorCounter) {
811             mSelectorAttributes.put(UiSelector.SELECTOR_COUNT, ++currentSelectorCounter);
812         }
813         return false;
814     }
815 
816     /**
817      * Leaf selector indicates no more child or parent selectors
818      * are declared in the this selector.
819      * @return true if is leaf.
820      */
isLeaf()821     boolean isLeaf() {
822         return mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_CHILD) < 0
823                 && mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_PARENT) < 0;
824     }
825 
hasChildSelector()826     boolean hasChildSelector() {
827         return mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_CHILD) >= 0;
828     }
829 
hasPatternSelector()830     boolean hasPatternSelector() {
831         return mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_PATTERN) >= 0;
832     }
833 
hasContainerSelector()834     boolean hasContainerSelector() {
835         return mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_CONTAINER) >= 0;
836     }
837 
hasParentSelector()838     boolean hasParentSelector() {
839         return mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_PARENT) >= 0;
840     }
841 
842     /**
843      * Returns the deepest selector in the chain of possible sub selectors.
844      * A chain of selector is created when either of {@link UiSelector#childSelector(UiSelector)}
845      * or {@link UiSelector#fromParent(UiSelector)} are used once or more in the construction of
846      * a selector.
847      * @return last UiSelector in chain
848      */
getLastSubSelector()849     private UiSelector getLastSubSelector() {
850         if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_CHILD) >= 0) {
851             UiSelector child = (UiSelector)mSelectorAttributes.get(UiSelector.SELECTOR_CHILD);
852             if (child.getLastSubSelector() == null) {
853                 return child;
854             }
855             return child.getLastSubSelector();
856         } else if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_PARENT) >= 0) {
857             UiSelector parent = (UiSelector)mSelectorAttributes.get(UiSelector.SELECTOR_PARENT);
858             if (parent.getLastSubSelector() == null) {
859                 return parent;
860             }
861             return parent.getLastSubSelector();
862         }
863         return this;
864     }
865 
866     @Override
toString()867     public String toString() {
868         return dumpToString(true);
869     }
870 
dumpToString(boolean all)871     String dumpToString(boolean all) {
872         StringBuilder builder = new StringBuilder();
873         builder.append(UiSelector.class.getSimpleName()).append("[");
874         final int criterionCount = mSelectorAttributes.size();
875         for (int i = 0; i < criterionCount; i++) {
876             if (i > 0) {
877                 builder.append(", ");
878             }
879             final int criterion = mSelectorAttributes.keyAt(i);
880             switch (criterion) {
881                 case SELECTOR_TEXT:
882                     builder.append("TEXT=").append(mSelectorAttributes.valueAt(i));
883                     break;
884                 case SELECTOR_TEXT_REGEX:
885                     builder.append("TEXT_REGEX=").append(mSelectorAttributes.valueAt(i));
886                     break;
887                 case SELECTOR_START_TEXT:
888                     builder.append("START_TEXT=").append(mSelectorAttributes.valueAt(i));
889                     break;
890                 case SELECTOR_CONTAINS_TEXT:
891                     builder.append("CONTAINS_TEXT=").append(mSelectorAttributes.valueAt(i));
892                     break;
893                 case SELECTOR_CLASS:
894                     builder.append("CLASS=").append(mSelectorAttributes.valueAt(i));
895                     break;
896                 case SELECTOR_CLASS_REGEX:
897                     builder.append("CLASS_REGEX=").append(mSelectorAttributes.valueAt(i));
898                     break;
899                 case SELECTOR_DESCRIPTION:
900                     builder.append("DESCRIPTION=").append(mSelectorAttributes.valueAt(i));
901                     break;
902                 case SELECTOR_DESCRIPTION_REGEX:
903                     builder.append("DESCRIPTION_REGEX=").append(mSelectorAttributes.valueAt(i));
904                     break;
905                 case SELECTOR_START_DESCRIPTION:
906                     builder.append("START_DESCRIPTION=").append(mSelectorAttributes.valueAt(i));
907                     break;
908                 case SELECTOR_CONTAINS_DESCRIPTION:
909                     builder.append("CONTAINS_DESCRIPTION=").append(mSelectorAttributes.valueAt(i));
910                     break;
911                 case SELECTOR_INDEX:
912                     builder.append("INDEX=").append(mSelectorAttributes.valueAt(i));
913                     break;
914                 case SELECTOR_INSTANCE:
915                     builder.append("INSTANCE=").append(mSelectorAttributes.valueAt(i));
916                     break;
917                 case SELECTOR_ENABLED:
918                     builder.append("ENABLED=").append(mSelectorAttributes.valueAt(i));
919                     break;
920                 case SELECTOR_FOCUSED:
921                     builder.append("FOCUSED=").append(mSelectorAttributes.valueAt(i));
922                     break;
923                 case SELECTOR_FOCUSABLE:
924                     builder.append("FOCUSABLE=").append(mSelectorAttributes.valueAt(i));
925                     break;
926                 case SELECTOR_SCROLLABLE:
927                     builder.append("SCROLLABLE=").append(mSelectorAttributes.valueAt(i));
928                     break;
929                 case SELECTOR_CLICKABLE:
930                     builder.append("CLICKABLE=").append(mSelectorAttributes.valueAt(i));
931                     break;
932                 case SELECTOR_CHECKABLE:
933                     builder.append("CHECKABLE=").append(mSelectorAttributes.valueAt(i));
934                     break;
935                 case SELECTOR_LONG_CLICKABLE:
936                     builder.append("LONG_CLICKABLE=").append(mSelectorAttributes.valueAt(i));
937                     break;
938                 case SELECTOR_CHECKED:
939                     builder.append("CHECKED=").append(mSelectorAttributes.valueAt(i));
940                     break;
941                 case SELECTOR_SELECTED:
942                     builder.append("SELECTED=").append(mSelectorAttributes.valueAt(i));
943                     break;
944                 case SELECTOR_ID:
945                     builder.append("ID=").append(mSelectorAttributes.valueAt(i));
946                     break;
947                 case SELECTOR_CHILD:
948                     if (all) {
949                         builder.append("CHILD=").append(mSelectorAttributes.valueAt(i));
950                     } else {
951                         builder.append("CHILD[..]");
952                     }
953                     break;
954                 case SELECTOR_PATTERN:
955                     if (all) {
956                         builder.append("PATTERN=").append(mSelectorAttributes.valueAt(i));
957                     } else {
958                         builder.append("PATTERN[..]");
959                     }
960                     break;
961                 case SELECTOR_CONTAINER:
962                     if (all) {
963                         builder.append("CONTAINER=").append(mSelectorAttributes.valueAt(i));
964                     } else {
965                         builder.append("CONTAINER[..]");
966                     }
967                     break;
968                 case SELECTOR_PARENT:
969                     if (all) {
970                         builder.append("PARENT=").append(mSelectorAttributes.valueAt(i));
971                     } else {
972                         builder.append("PARENT[..]");
973                     }
974                     break;
975                 case SELECTOR_COUNT:
976                     builder.append("COUNT=").append(mSelectorAttributes.valueAt(i));
977                     break;
978                 case SELECTOR_PACKAGE_NAME:
979                     builder.append("PACKAGE_NAME=").append(mSelectorAttributes.valueAt(i));
980                     break;
981                 case SELECTOR_PACKAGE_NAME_REGEX:
982                     builder.append("PACKAGE_NAME_REGEX=").append(mSelectorAttributes.valueAt(i));
983                     break;
984                 case SELECTOR_RESOURCE_ID:
985                     builder.append("RESOURCE_ID=").append(mSelectorAttributes.valueAt(i));
986                     break;
987                 case SELECTOR_RESOURCE_ID_REGEX:
988                     builder.append("RESOURCE_ID_REGEX=").append(mSelectorAttributes.valueAt(i));
989                     break;
990                 default:
991                     builder.append("UNDEFINED=").append(criterion).append(" ").append(
992                             mSelectorAttributes.valueAt(i));
993             }
994         }
995         builder.append("]");
996         return builder.toString();
997     }
998 }
999