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