1 package org.robolectric.shadows; 2 3 import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2; 4 import static android.os.Build.VERSION_CODES.KITKAT; 5 import static android.os.Build.VERSION_CODES.LOLLIPOP; 6 import static android.os.Build.VERSION_CODES.LOLLIPOP_MR1; 7 import static org.robolectric.RuntimeEnvironment.getApiLevel; 8 import static org.robolectric.Shadows.shadowOf; 9 10 import android.graphics.Rect; 11 import android.os.Bundle; 12 import android.os.Parcel; 13 import android.os.Parcelable; 14 import android.util.Pair; 15 import android.util.SparseArray; 16 import android.view.View; 17 import android.view.accessibility.AccessibilityNodeInfo; 18 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; 19 import android.view.accessibility.AccessibilityNodeInfo.CollectionInfo; 20 import android.view.accessibility.AccessibilityNodeInfo.CollectionItemInfo; 21 import android.view.accessibility.AccessibilityNodeInfo.RangeInfo; 22 import android.view.accessibility.AccessibilityWindowInfo; 23 import java.util.ArrayList; 24 import java.util.Collections; 25 import java.util.HashMap; 26 import java.util.Iterator; 27 import java.util.List; 28 import java.util.Map; 29 import org.robolectric.RuntimeEnvironment; 30 import org.robolectric.annotation.Implementation; 31 import org.robolectric.annotation.Implements; 32 import org.robolectric.annotation.RealObject; 33 import org.robolectric.util.ReflectionHelpers; 34 import org.robolectric.util.ReflectionHelpers.ClassParameter; 35 36 /** 37 * Shadow of {@link android.view.accessibility.AccessibilityNodeInfo} that allows a test to set 38 * properties that are locked in the original class. It also keeps track of calls to 39 * {@code obtain()} and {@code recycle()} to look for bugs that mismatches. 40 */ 41 @Implements(AccessibilityNodeInfo.class) 42 public class ShadowAccessibilityNodeInfo { 43 // Map of obtained instances of the class along with stack traces of how they were obtained 44 private static final Map<StrictEqualityNodeWrapper, StackTraceElement[]> obtainedInstances = 45 new HashMap<>(); 46 47 private static final SparseArray<StrictEqualityNodeWrapper> orderedInstances = new SparseArray<>(); 48 49 // Bitmasks for actions 50 public static final int UNDEFINED_SELECTION_INDEX = -1; 51 52 public static final Parcelable.Creator<AccessibilityNodeInfo> CREATOR = 53 new Parcelable.Creator<AccessibilityNodeInfo>() { 54 55 @Override 56 public AccessibilityNodeInfo createFromParcel(Parcel source) { 57 return obtain(orderedInstances.get(source.readInt()).mInfo); 58 } 59 60 @Override 61 public AccessibilityNodeInfo[] newArray(int size) { 62 return new AccessibilityNodeInfo[size]; 63 }}; 64 65 private static int sAllocationCount = 0; 66 67 private static final int CLICKABLE_MASK = 0x00000001; 68 69 private static final int LONGCLICKABLE_MASK = 0x00000002; 70 71 private static final int FOCUSABLE_MASK = 0x00000004; 72 73 private static final int FOCUSED_MASK = 0x00000008; 74 75 private static final int VISIBLE_TO_USER_MASK = 0x00000010; 76 77 private static final int SCROLLABLE_MASK = 0x00000020; 78 79 private static final int PASTEABLE_MASK = 0x00000040; 80 81 private static final int EDITABLE_MASK = 0x00000080; 82 83 private static final int TEXT_SELECTION_SETABLE_MASK = 0x00000100; 84 85 private static final int CHECKABLE_MASK = 0x00001000; //14 86 87 private static final int CHECKED_MASK = 0x00002000; //14 88 89 private static final int ENABLED_MASK = 0x00010000; //14 90 91 private static final int PASSWORD_MASK = 0x00040000; //14 92 93 private static final int SELECTED_MASK = 0x00080000; //14 94 95 private static final int A11YFOCUSED_MASK = 0x00000800; //16 96 97 private static final int MULTILINE_MASK = 0x00020000; //19 98 99 private static final int CONTENT_INVALID_MASK = 0x00004000; //19 100 101 private static final int DISMISSABLE_MASK = 0x00008000; //19 102 103 private static final int CAN_OPEN_POPUP_MASK = 0x00100000; //19 104 105 /** 106 * Uniquely identifies the origin of the AccessibilityNodeInfo for equality 107 * testing. Two instances that come from the same node info should have the 108 * same ID. 109 */ 110 private long mOriginNodeId; 111 112 private List<AccessibilityNodeInfo> children; 113 114 private Rect boundsInScreen = new Rect(); 115 116 private Rect boundsInParent = new Rect(); 117 118 private List<Pair<Integer, Bundle>> performedActionAndArgsList; 119 120 // In API prior to 21, actions are stored in a flag, after 21 they are stored in array of 121 // AccessibilityAction so custom actions can be supported. 122 private ArrayList<AccessibilityAction> actionsArray; 123 private int actionsMask; 124 // Storage of flags 125 126 private int propertyFlags; 127 128 private AccessibilityNodeInfo parent; 129 130 private AccessibilityNodeInfo labelFor; 131 132 private AccessibilityNodeInfo labeledBy; 133 134 private View view; 135 136 private CharSequence contentDescription; 137 138 private CharSequence text; 139 140 private CharSequence className; 141 142 private int textSelectionStart = UNDEFINED_SELECTION_INDEX; 143 144 private int textSelectionEnd = UNDEFINED_SELECTION_INDEX; 145 146 private boolean refreshReturnValue = true; 147 148 private int movementGranularities; //16 149 150 private CharSequence packageName; //14 151 152 private String viewIdResourceName; //18 153 154 private CollectionInfo collectionInfo; //19 155 156 private CollectionItemInfo collectionItemInfo; //19 157 158 private int inputType; //19 159 160 private int liveRegion; //19 161 162 private RangeInfo rangeInfo; //19 163 164 private int maxTextLength; //21 165 166 private CharSequence error; //21 167 168 private AccessibilityWindowInfo accessibilityWindowInfo; 169 170 private AccessibilityNodeInfo traversalAfter; //22 171 172 private AccessibilityNodeInfo traversalBefore; //22 173 174 private OnPerformActionListener actionListener; 175 176 @RealObject 177 private AccessibilityNodeInfo realAccessibilityNodeInfo; 178 179 @Implementation 180 public void __constructor__() { 181 ReflectionHelpers.setStaticField(AccessibilityNodeInfo.class, "CREATOR", ShadowAccessibilityNodeInfo.CREATOR); 182 } 183 184 @Implementation 185 public static AccessibilityNodeInfo obtain(AccessibilityNodeInfo info) { 186 final ShadowAccessibilityNodeInfo shadowInfo = shadowOf(info); 187 final AccessibilityNodeInfo obtainedInstance = shadowInfo.getClone(); 188 189 sAllocationCount++; 190 if (shadowInfo.mOriginNodeId == 0) { 191 shadowInfo.mOriginNodeId = sAllocationCount; 192 } 193 StrictEqualityNodeWrapper wrapper = new StrictEqualityNodeWrapper(obtainedInstance); 194 obtainedInstances.put(wrapper, Thread.currentThread().getStackTrace()); 195 orderedInstances.put(sAllocationCount, wrapper); 196 return obtainedInstance; 197 } 198 199 @Implementation 200 public static AccessibilityNodeInfo obtain(View view) { 201 // We explicitly avoid allocating the AccessibilityNodeInfo from the actual pool by using the 202 // private constructor. Not doing so affects test suites which use both shadow and 203 // non-shadow objects. 204 final AccessibilityNodeInfo obtainedInstance = 205 ReflectionHelpers.callConstructor(AccessibilityNodeInfo.class); 206 final ShadowAccessibilityNodeInfo shadowObtained = shadowOf(obtainedInstance); 207 208 /* 209 * We keep a separate list of actions for each object newly obtained 210 * from a view, and perform a shallow copy during getClone. That way the 211 * list of actions performed contains all actions performed on the view 212 * by the tree of nodes initialized from it. Note that initializing two 213 * nodes with the same view will not merge the two lists, as so the list 214 * of performed actions will not contain all actions performed on the 215 * underlying view. 216 */ 217 shadowObtained.performedActionAndArgsList = new ArrayList<>(); 218 219 shadowObtained.view = view; 220 sAllocationCount++; 221 if (shadowObtained.mOriginNodeId == 0) { 222 shadowObtained.mOriginNodeId = sAllocationCount; 223 } 224 StrictEqualityNodeWrapper wrapper = new StrictEqualityNodeWrapper(obtainedInstance); 225 obtainedInstances.put(wrapper, Thread.currentThread().getStackTrace()); 226 orderedInstances.put(sAllocationCount, wrapper); 227 return obtainedInstance; 228 } 229 230 @Implementation 231 public static AccessibilityNodeInfo obtain() { 232 return obtain(new View(RuntimeEnvironment.application.getApplicationContext())); 233 } 234 235 @Implementation 236 public static AccessibilityNodeInfo obtain(View root, int virtualDescendantId) { 237 AccessibilityNodeInfo node = obtain(root); 238 return node; 239 } 240 241 /** 242 * Check for leaked objects that were {@code obtain}ed but never 243 * {@code recycle}d. 244 * 245 * @param printUnrecycledNodesToSystemErr - if true, stack traces of calls 246 * to {@code obtain} that lack matching calls to {@code recycle} are 247 * dumped to System.err. 248 * @return {@code true} if there are unrecycled nodes 249 */ 250 public static boolean areThereUnrecycledNodes(boolean printUnrecycledNodesToSystemErr) { 251 if (printUnrecycledNodesToSystemErr) { 252 for (final StrictEqualityNodeWrapper wrapper : obtainedInstances.keySet()) { 253 final ShadowAccessibilityNodeInfo shadow = shadowOf(wrapper.mInfo); 254 255 System.err.println(String.format( 256 "Leaked contentDescription = %s. Stack trace:", shadow.getContentDescription())); 257 for (final StackTraceElement stackTraceElement : obtainedInstances.get(wrapper)) { 258 System.err.println(stackTraceElement.toString()); 259 } 260 } 261 } 262 263 return (obtainedInstances.size() != 0); 264 } 265 266 /** 267 * Clear list of obtained instance objects. {@code areThereUnrecycledNodes} 268 * will always return false if called immediately afterwards. 269 */ 270 public static void resetObtainedInstances() { 271 obtainedInstances.clear(); 272 orderedInstances.clear(); 273 } 274 275 @Implementation 276 public void recycle() { 277 final StrictEqualityNodeWrapper wrapper = 278 new StrictEqualityNodeWrapper(realAccessibilityNodeInfo); 279 if (!obtainedInstances.containsKey(wrapper)) { 280 throw new IllegalStateException(); 281 } 282 283 if (labelFor != null) { 284 labelFor.recycle(); 285 } 286 287 if (labeledBy != null) { 288 labeledBy.recycle(); 289 } 290 if (getApiLevel() >= LOLLIPOP_MR1) { 291 if (traversalAfter != null) { 292 traversalAfter.recycle(); 293 } 294 295 if (traversalBefore != null) { 296 traversalBefore.recycle(); 297 } 298 } 299 300 obtainedInstances.remove(wrapper); 301 int keyOfWrapper = -1; 302 for (int i = 0; i < orderedInstances.size(); i++) { 303 int key = orderedInstances.keyAt(i); 304 if (orderedInstances.get(key).equals(wrapper)) { 305 keyOfWrapper = key; 306 break; 307 } 308 } 309 orderedInstances.remove(keyOfWrapper); 310 } 311 312 @Implementation 313 public int getChildCount() { 314 if (children == null) { 315 return 0; 316 } 317 318 return children.size(); 319 } 320 321 @Implementation 322 public AccessibilityNodeInfo getChild(int index) { 323 if (children == null) { 324 return null; 325 } 326 327 final AccessibilityNodeInfo child = children.get(index); 328 if (child == null) { 329 return null; 330 } 331 332 return obtain(child); 333 } 334 335 @Implementation 336 public AccessibilityNodeInfo getParent() { 337 if (parent == null) { 338 return null; 339 } 340 341 return obtain(parent); 342 } 343 344 @Implementation 345 public boolean refresh() { 346 return refreshReturnValue; 347 } 348 349 public void setRefreshReturnValue(boolean refreshReturnValue) { 350 this.refreshReturnValue = refreshReturnValue; 351 } 352 353 @Implementation 354 public boolean isClickable() { 355 return ((propertyFlags & CLICKABLE_MASK) != 0); 356 } 357 358 @Implementation 359 public boolean isLongClickable() { 360 return ((propertyFlags & LONGCLICKABLE_MASK) != 0); 361 } 362 363 @Implementation 364 public boolean isFocusable() { 365 return ((propertyFlags & FOCUSABLE_MASK) != 0); 366 } 367 368 @Implementation 369 public boolean isFocused() { 370 return ((propertyFlags & FOCUSED_MASK) != 0); 371 } 372 373 @Implementation 374 public boolean isVisibleToUser() { 375 return ((propertyFlags & VISIBLE_TO_USER_MASK) != 0); 376 } 377 378 @Implementation 379 public boolean isScrollable() { 380 return ((propertyFlags & SCROLLABLE_MASK) != 0); 381 } 382 383 public boolean isPasteable() { 384 return ((propertyFlags & PASTEABLE_MASK) != 0); 385 } 386 387 @Implementation 388 public boolean isEditable() { 389 return ((propertyFlags & EDITABLE_MASK) != 0); 390 } 391 392 public boolean isTextSelectionSetable() { 393 return ((propertyFlags & TEXT_SELECTION_SETABLE_MASK) != 0); 394 } 395 396 @Implementation 397 public boolean isCheckable() { 398 return ((propertyFlags & CHECKABLE_MASK) != 0); 399 } 400 401 @Implementation 402 public void setCheckable(boolean checkable) { 403 propertyFlags = (propertyFlags & ~CHECKABLE_MASK) | 404 (checkable ? CHECKABLE_MASK : 0); 405 } 406 407 @Implementation 408 public void setChecked(boolean checked) { 409 propertyFlags = (propertyFlags & ~CHECKED_MASK) | 410 (checked ? CHECKED_MASK : 0); 411 } 412 413 @Implementation 414 public boolean isChecked() { 415 return ((propertyFlags & CHECKED_MASK) != 0); 416 } 417 418 @Implementation 419 public void setEnabled(boolean enabled) { 420 propertyFlags = (propertyFlags & ~ENABLED_MASK) | 421 (enabled ? ENABLED_MASK : 0); 422 } 423 424 @Implementation 425 public boolean isEnabled() { 426 return ((propertyFlags & ENABLED_MASK) != 0); 427 } 428 429 @Implementation 430 public void setPassword(boolean password) { 431 propertyFlags = (propertyFlags & ~PASSWORD_MASK) | 432 (password ? PASSWORD_MASK : 0); 433 } 434 435 @Implementation 436 public boolean isPassword() { 437 return ((propertyFlags & PASSWORD_MASK) != 0); 438 } 439 440 @Implementation 441 public void setSelected(boolean selected) { 442 propertyFlags = (propertyFlags & ~SELECTED_MASK) | 443 (selected ? SELECTED_MASK : 0); 444 } 445 446 @Implementation 447 public boolean isSelected() { 448 return ((propertyFlags & SELECTED_MASK) != 0); 449 } 450 451 @Implementation 452 public void setAccessibilityFocused(boolean focused) { 453 propertyFlags = (propertyFlags & ~A11YFOCUSED_MASK) | 454 (focused ? A11YFOCUSED_MASK : 0); 455 } 456 457 @Implementation 458 public boolean isAccessibilityFocused() { 459 return ((propertyFlags & A11YFOCUSED_MASK) != 0); 460 } 461 462 @Implementation(minSdk = LOLLIPOP) 463 public void setMultiLine(boolean multiLine) { 464 propertyFlags = (propertyFlags & ~MULTILINE_MASK) | 465 (multiLine ? MULTILINE_MASK : 0); 466 } 467 468 @Implementation(minSdk = LOLLIPOP) 469 public boolean isMultiLine() { 470 return ((propertyFlags & MULTILINE_MASK) != 0); 471 } 472 473 @Implementation(minSdk = LOLLIPOP) 474 public void setContentInvalid(boolean contentInvalid) { 475 propertyFlags = (propertyFlags & ~CONTENT_INVALID_MASK) | 476 (contentInvalid ? CONTENT_INVALID_MASK : 0); 477 } 478 479 @Implementation(minSdk = LOLLIPOP) 480 public boolean isContentInvalid() { 481 return ((propertyFlags & CONTENT_INVALID_MASK) != 0); 482 } 483 484 @Implementation(minSdk = LOLLIPOP) 485 public void setDismissable(boolean dismissable) { 486 propertyFlags = (propertyFlags & ~DISMISSABLE_MASK) | 487 (dismissable ? DISMISSABLE_MASK : 0); 488 } 489 490 @Implementation(minSdk = LOLLIPOP) 491 public boolean isDismissable() { 492 return ((propertyFlags & DISMISSABLE_MASK) != 0); 493 } 494 495 @Implementation(minSdk = LOLLIPOP) 496 public void setCanOpenPopup(boolean opensPopup) { 497 propertyFlags = (propertyFlags & ~CAN_OPEN_POPUP_MASK) | 498 (opensPopup ? CAN_OPEN_POPUP_MASK : 0); 499 } 500 501 @Implementation(minSdk = LOLLIPOP) 502 public boolean canOpenPopup() { 503 return ((propertyFlags & CAN_OPEN_POPUP_MASK) != 0); 504 } 505 506 public void setTextSelectionSetable(boolean isTextSelectionSetable) { 507 propertyFlags = (propertyFlags & ~TEXT_SELECTION_SETABLE_MASK) | 508 (isTextSelectionSetable ? TEXT_SELECTION_SETABLE_MASK : 0); 509 } 510 511 @Implementation 512 public void setClickable(boolean isClickable) { 513 propertyFlags = (propertyFlags & ~CLICKABLE_MASK) | (isClickable ? CLICKABLE_MASK : 0); 514 } 515 516 @Implementation 517 public void setLongClickable(boolean isLongClickable) { 518 propertyFlags = 519 (propertyFlags & ~LONGCLICKABLE_MASK) | (isLongClickable ? LONGCLICKABLE_MASK : 0); 520 } 521 522 @Implementation 523 public void setFocusable(boolean isFocusable) { 524 propertyFlags = (propertyFlags & ~FOCUSABLE_MASK) | (isFocusable ? FOCUSABLE_MASK : 0); 525 } 526 527 @Implementation 528 public void setFocused(boolean isFocused) { 529 propertyFlags = (propertyFlags & ~FOCUSED_MASK) | (isFocused ? FOCUSED_MASK : 0); 530 } 531 532 @Implementation 533 public void setScrollable(boolean isScrollable) { 534 propertyFlags = (propertyFlags & ~SCROLLABLE_MASK) | (isScrollable ? SCROLLABLE_MASK : 0); 535 } 536 537 public void setPasteable(boolean isPasteable) { 538 propertyFlags = (propertyFlags & ~PASTEABLE_MASK) | (isPasteable ? PASTEABLE_MASK : 0); 539 } 540 541 @Implementation 542 public void setEditable(boolean isEditable) { 543 propertyFlags = (propertyFlags & ~EDITABLE_MASK) | (isEditable ? EDITABLE_MASK : 0); 544 } 545 546 @Implementation 547 public void setVisibleToUser(boolean isVisibleToUser) { 548 propertyFlags = 549 (propertyFlags & ~VISIBLE_TO_USER_MASK) | (isVisibleToUser ? VISIBLE_TO_USER_MASK : 0); 550 } 551 552 @Implementation 553 public void setContentDescription(CharSequence description) { 554 contentDescription = description; 555 } 556 557 @Implementation 558 public CharSequence getContentDescription() { 559 return contentDescription; 560 } 561 562 @Implementation 563 public void setClassName(CharSequence name) { 564 className = name; 565 } 566 567 @Implementation 568 public CharSequence getClassName() { 569 return className; 570 } 571 572 @Implementation 573 public void setText(CharSequence t) { 574 text = t; 575 } 576 577 @Implementation 578 public CharSequence getText() { 579 return text; 580 } 581 582 @Implementation 583 public void setTextSelection(int start, int end) { 584 textSelectionStart = start; 585 textSelectionEnd = end; 586 } 587 588 /** 589 * Gets the text selection start. 590 * 591 * @return The text selection start if there is selection or UNDEFINED_SELECTION_INDEX. 592 */ 593 @Implementation 594 public int getTextSelectionStart() { 595 return textSelectionStart; 596 } 597 598 /** 599 * Gets the text selection end. 600 * 601 * @return The text selection end if there is selection or UNDEFINED_SELECTION_INDEX. 602 */ 603 @Implementation 604 public int getTextSelectionEnd() { 605 return textSelectionEnd; 606 } 607 608 @Implementation 609 public AccessibilityNodeInfo getLabelFor() { 610 if (labelFor == null) { 611 return null; 612 } 613 614 return obtain(labelFor); 615 } 616 617 public void setLabelFor(AccessibilityNodeInfo info) { 618 if (labelFor != null) { 619 labelFor.recycle(); 620 } 621 622 labelFor = obtain(info); 623 } 624 625 @Implementation 626 public AccessibilityNodeInfo getLabeledBy() { 627 if (labeledBy == null) { 628 return null; 629 } 630 631 return obtain(labeledBy); 632 } 633 634 public void setLabeledBy(AccessibilityNodeInfo info) { 635 if (labeledBy != null) { 636 labeledBy.recycle(); 637 } 638 639 labeledBy = obtain(info); 640 } 641 642 @Implementation 643 public int getMovementGranularities() { 644 return movementGranularities; 645 } 646 647 @Implementation 648 public void setMovementGranularities(int movementGranularities) { 649 this.movementGranularities = movementGranularities; 650 } 651 652 @Implementation 653 public CharSequence getPackageName() { 654 return packageName; 655 } 656 657 @Implementation 658 public void setPackageName(CharSequence packageName) { 659 this.packageName = packageName; 660 } 661 662 @Implementation(minSdk = JELLY_BEAN_MR2) 663 public String getViewIdResourceName() { 664 return viewIdResourceName; 665 } 666 667 @Implementation(minSdk = JELLY_BEAN_MR2) 668 public void setViewIdResourceName(String viewIdResourceName) { 669 this.viewIdResourceName = viewIdResourceName; 670 } 671 672 @Implementation(minSdk = KITKAT) 673 public CollectionInfo getCollectionInfo() { 674 return collectionInfo; 675 } 676 677 @Implementation(minSdk = KITKAT) 678 public void setCollectionInfo(CollectionInfo collectionInfo) { 679 this.collectionInfo = collectionInfo; 680 } 681 682 @Implementation(minSdk = KITKAT) 683 public CollectionItemInfo getCollectionItemInfo() { 684 return collectionItemInfo; 685 } 686 687 @Implementation(minSdk = KITKAT) 688 public void setCollectionItemInfo(CollectionItemInfo collectionItemInfo) { 689 this.collectionItemInfo = collectionItemInfo; 690 } 691 692 @Implementation(minSdk = KITKAT) 693 public int getInputType() { 694 return inputType; 695 } 696 697 @Implementation(minSdk = KITKAT) 698 public void setInputType(int inputType) { 699 this.inputType = inputType; 700 } 701 702 @Implementation(minSdk = KITKAT) 703 public int getLiveRegion() { 704 return liveRegion; 705 } 706 707 @Implementation(minSdk = KITKAT) 708 public void setLiveRegion(int liveRegion) { 709 this.liveRegion = liveRegion; 710 } 711 712 @Implementation(minSdk = KITKAT) 713 public RangeInfo getRangeInfo() { 714 return rangeInfo; 715 } 716 717 @Implementation(minSdk = KITKAT) 718 public void setRangeInfo(RangeInfo rangeInfo) { 719 this.rangeInfo = rangeInfo; 720 } 721 722 @Implementation(minSdk = LOLLIPOP) 723 public int getMaxTextLength() { 724 return maxTextLength; 725 } 726 727 @Implementation(minSdk = LOLLIPOP) 728 public void setMaxTextLength(int maxTextLength) { 729 this.maxTextLength = maxTextLength; 730 } 731 732 @Implementation(minSdk = LOLLIPOP) 733 public CharSequence getError() { 734 return error; 735 } 736 737 @Implementation(minSdk = LOLLIPOP) 738 public void setError(CharSequence error) { 739 this.error = error; 740 } 741 742 @Implementation(minSdk = LOLLIPOP_MR1) 743 public AccessibilityNodeInfo getTraversalAfter() { 744 if (traversalAfter == null) { 745 return null; 746 } 747 748 return obtain(traversalAfter); 749 } 750 751 @Implementation(minSdk = LOLLIPOP_MR1) 752 public void setTraversalAfter(AccessibilityNodeInfo info) { 753 if (this.traversalAfter != null) { 754 this.traversalAfter.recycle(); 755 } 756 757 this.traversalAfter = obtain(info); 758 } 759 760 @Implementation(minSdk = LOLLIPOP_MR1) 761 public AccessibilityNodeInfo getTraversalBefore() { 762 if (traversalBefore == null) { 763 return null; 764 } 765 766 return obtain(traversalBefore); 767 } 768 769 @Implementation(minSdk = LOLLIPOP_MR1) 770 public void setTraversalBefore(AccessibilityNodeInfo info) { 771 if (this.traversalBefore != null) { 772 this.traversalBefore.recycle(); 773 } 774 775 this.traversalBefore = obtain(info); 776 } 777 778 @Implementation 779 public void setSource (View source) { 780 this.view = source; 781 } 782 783 @Implementation 784 public void setSource (View root, int virtualDescendantId) { 785 this.view = root; 786 } 787 788 @Implementation 789 public void getBoundsInScreen(Rect outBounds) { 790 if (boundsInScreen == null) { 791 boundsInScreen = new Rect(); 792 } 793 outBounds.set(boundsInScreen); 794 } 795 796 @Implementation 797 public void getBoundsInParent(Rect outBounds) { 798 if (boundsInParent == null) { 799 boundsInParent = new Rect(); 800 } 801 outBounds.set(boundsInParent); 802 } 803 804 @Implementation 805 public void setBoundsInScreen(Rect b) { 806 if (boundsInScreen == null) { 807 boundsInScreen = new Rect(b); 808 } else { 809 boundsInScreen.set(b); 810 } 811 } 812 813 @Implementation 814 public void setBoundsInParent(Rect b) { 815 if (boundsInParent == null) { 816 boundsInParent = new Rect(b); 817 } else { 818 boundsInParent.set(b); 819 } 820 } 821 822 @Implementation 823 public void addAction(int action) { 824 if (getApiLevel() >= LOLLIPOP) { 825 if ((action & getActionTypeMaskFromFramework()) != 0) { 826 throw new IllegalArgumentException("Action is not a combination of the standard " + 827 "actions: " + action); 828 } 829 int remainingIds = action; 830 while (remainingIds > 0) { 831 final int id = 1 << Integer.numberOfTrailingZeros(remainingIds); 832 remainingIds &= ~id; 833 AccessibilityAction convertedAction = getActionFromIdFromFrameWork(id); 834 addAction(convertedAction); 835 } 836 } else { 837 actionsMask |= action; 838 } 839 } 840 841 @Implementation(minSdk = LOLLIPOP) 842 public void addAction(AccessibilityAction action) { 843 if (action == null) { 844 return; 845 } 846 847 if (actionsArray == null) { 848 actionsArray = new ArrayList<>(); 849 } 850 actionsArray.remove(action); 851 actionsArray.add(action); 852 } 853 854 @Implementation(minSdk = LOLLIPOP) 855 public void removeAction(int action) { 856 AccessibilityAction convertedAction = getActionFromIdFromFrameWork(action); 857 removeAction(convertedAction); 858 } 859 860 @Implementation(minSdk = LOLLIPOP) 861 public boolean removeAction(AccessibilityAction action) { 862 if (action == null || actionsArray == null) { 863 return false; 864 } 865 return actionsArray.remove(action); 866 } 867 868 /** 869 * Obtain flags for actions supported. Currently only supports 870 * {@link AccessibilityNodeInfo#ACTION_CLICK}, 871 * {@link AccessibilityNodeInfo#ACTION_LONG_CLICK}, 872 * {@link AccessibilityNodeInfo#ACTION_SCROLL_FORWARD}, 873 * {@link AccessibilityNodeInfo#ACTION_PASTE}, 874 * {@link AccessibilityNodeInfo#ACTION_FOCUS}, 875 * {@link AccessibilityNodeInfo#ACTION_SET_SELECTION}, 876 * {@link AccessibilityNodeInfo#ACTION_SCROLL_BACKWARD} 877 * Returned value is derived from the getters. 878 * 879 * @return Action mask. 0 if no actions supported. 880 */ 881 @Implementation 882 public int getActions() { 883 if (getApiLevel() >= LOLLIPOP) { 884 int returnValue = 0; 885 if (actionsArray == null) { 886 return returnValue; 887 } 888 889 // Custom actions are only returned by getActionsList 890 final int actionSize = actionsArray.size(); 891 for (int i = 0; i < actionSize; i++) { 892 int actionId = actionsArray.get(i).getId(); 893 if (actionId <= getLastLegacyActionFromFrameWork()) { 894 returnValue |= actionId; 895 } 896 } 897 return returnValue; 898 } else { 899 return actionsMask; 900 } 901 } 902 903 @Implementation(minSdk = LOLLIPOP) 904 public AccessibilityWindowInfo getWindow() { 905 return accessibilityWindowInfo; 906 } 907 908 public void setAccessibilityWindowInfo(AccessibilityWindowInfo info) { 909 accessibilityWindowInfo = info; 910 } 911 912 @Implementation(minSdk = LOLLIPOP) 913 public List<AccessibilityAction> getActionList() { 914 if (actionsArray == null) { 915 return Collections.emptyList(); 916 } 917 918 return actionsArray; 919 } 920 921 @Implementation 922 public boolean performAction(int action) { 923 return performAction(action, null); 924 } 925 926 @Implementation 927 public boolean performAction(int action, Bundle arguments) { 928 if (performedActionAndArgsList == null) { 929 performedActionAndArgsList = new ArrayList<>(); 930 } 931 932 performedActionAndArgsList.add(new Pair<>(action, arguments)); 933 return actionListener == null || actionListener.onPerformAccessibilityAction(action, arguments); 934 } 935 936 /** 937 * Equality check based on reference equality of the Views from which these instances were 938 * created, or the equality of their assigned IDs. 939 */ 940 @Implementation 941 @Override 942 public boolean equals(Object object) { 943 if (!(object instanceof AccessibilityNodeInfo)) { 944 return false; 945 } 946 947 final AccessibilityNodeInfo info = (AccessibilityNodeInfo) object; 948 final ShadowAccessibilityNodeInfo otherShadow = shadowOf(info); 949 950 if (this.view != null) { 951 return this.view == otherShadow.view; 952 } 953 if (this.mOriginNodeId != 0) { 954 return this.mOriginNodeId == otherShadow.mOriginNodeId; 955 } 956 throw new IllegalStateException("Node has neither an ID nor View"); 957 } 958 959 @Implementation 960 @Override 961 public int hashCode() { 962 // This is 0 for a reason. If you change it, you will break the obtained 963 // instances map in a manner that is remarkably difficult to debug. 964 // Having a dynamic hash code keeps this object from being located 965 // in the map if it was mutated after being obtained. 966 return 0; 967 } 968 969 /** 970 * Add a child node to this one. Also initializes the parent field of the 971 * child. 972 * 973 * @param child The node to be added as a child. 974 */ 975 public void addChild(AccessibilityNodeInfo child) { 976 if (children == null) { 977 children = new ArrayList<>(); 978 } 979 980 children.add(child); 981 (shadowOf(child)).parent = realAccessibilityNodeInfo; 982 } 983 984 @Implementation 985 public void addChild(View child) { 986 AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain(child); 987 addChild(node); 988 } 989 990 @Implementation 991 public void addChild(View root, int virtualDescendantId) { 992 AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain(root, virtualDescendantId); 993 addChild(node); 994 } 995 996 /** 997 * @return The list of arguments for the various calls to performAction. Unmodifiable. 998 */ 999 public List<Integer> getPerformedActions() { 1000 if (performedActionAndArgsList == null) { 1001 performedActionAndArgsList = new ArrayList<>(); 1002 } 1003 1004 // Here we take the actions out of the pairs and stick them into a separate LinkedList to return 1005 List<Integer> actionsOnly = new ArrayList<>(); 1006 Iterator<Pair<Integer, Bundle>> iter = performedActionAndArgsList.iterator(); 1007 while (iter.hasNext()) { 1008 actionsOnly.add(iter.next().first); 1009 } 1010 1011 return Collections.unmodifiableList(actionsOnly); 1012 } 1013 1014 /** 1015 * @return The list of arguments for the various calls to performAction. Unmodifiable. 1016 */ 1017 public List<Pair<Integer, Bundle>> getPerformedActionsWithArgs() { 1018 if (performedActionAndArgsList == null) { 1019 performedActionAndArgsList = new ArrayList<>(); 1020 } 1021 return Collections.unmodifiableList(performedActionAndArgsList); 1022 } 1023 1024 /** 1025 * @return A shallow copy. 1026 */ 1027 private AccessibilityNodeInfo getClone() { 1028 // We explicitly avoid allocating the AccessibilityNodeInfo from the actual pool by using 1029 // the private constructor. Not doing so affects test suites which use both shadow and 1030 // non-shadow objects. 1031 final AccessibilityNodeInfo newInfo = 1032 ReflectionHelpers.callConstructor(AccessibilityNodeInfo.class); 1033 final ShadowAccessibilityNodeInfo newShadow = shadowOf(newInfo); 1034 1035 newShadow.mOriginNodeId = mOriginNodeId; 1036 newShadow.boundsInScreen = new Rect(boundsInScreen); 1037 newShadow.propertyFlags = propertyFlags; 1038 newShadow.contentDescription = contentDescription; 1039 newShadow.text = text; 1040 newShadow.performedActionAndArgsList = performedActionAndArgsList; 1041 newShadow.parent = parent; 1042 newShadow.className = className; 1043 newShadow.labelFor = labelFor; 1044 newShadow.labeledBy = labeledBy; 1045 newShadow.view = view; 1046 newShadow.textSelectionStart = textSelectionStart; 1047 newShadow.textSelectionEnd = textSelectionEnd; 1048 newShadow.actionListener = actionListener; 1049 if (getApiLevel() >= LOLLIPOP) { 1050 if (actionsArray != null) { 1051 newShadow.actionsArray = new ArrayList<>(); 1052 newShadow.actionsArray.addAll(actionsArray); 1053 } else { 1054 newShadow.actionsArray = null; 1055 } 1056 } else { 1057 newShadow.actionsMask = actionsMask; 1058 } 1059 1060 if (children != null) { 1061 newShadow.children = new ArrayList<>(); 1062 newShadow.children.addAll(children); 1063 } else { 1064 newShadow.children = null; 1065 } 1066 1067 newShadow.refreshReturnValue = refreshReturnValue; 1068 newShadow.movementGranularities = movementGranularities; 1069 newShadow.packageName = packageName; 1070 if (getApiLevel() >= JELLY_BEAN_MR2) { 1071 newShadow.viewIdResourceName = viewIdResourceName; 1072 } 1073 if (getApiLevel() >= KITKAT) { 1074 newShadow.collectionInfo = collectionInfo; 1075 newShadow.collectionItemInfo = collectionItemInfo; 1076 newShadow.inputType = inputType; 1077 newShadow.liveRegion = liveRegion; 1078 newShadow.rangeInfo = rangeInfo; 1079 } 1080 if (getApiLevel() >= LOLLIPOP) { 1081 newShadow.maxTextLength = maxTextLength; 1082 newShadow.error = error; 1083 } 1084 if (getApiLevel() >= LOLLIPOP_MR1) { 1085 newShadow.traversalAfter = (traversalAfter == null) ? null : obtain(traversalAfter); 1086 newShadow.traversalBefore = (traversalBefore == null) ? null : obtain(traversalBefore); 1087 } 1088 return newInfo; 1089 } 1090 1091 /** 1092 * Private class to keep different nodes referring to the same view straight 1093 * in the mObtainedInstances map. 1094 */ 1095 private static class StrictEqualityNodeWrapper { 1096 public final AccessibilityNodeInfo mInfo; 1097 1098 public StrictEqualityNodeWrapper(AccessibilityNodeInfo info) { 1099 mInfo = info; 1100 } 1101 1102 @Override 1103 @SuppressWarnings("ReferenceEquality") 1104 public boolean equals(Object object) { 1105 if (object == null) { 1106 return false; 1107 } 1108 1109 final StrictEqualityNodeWrapper wrapper = (StrictEqualityNodeWrapper) object; 1110 return mInfo == wrapper.mInfo; 1111 } 1112 1113 @Override 1114 public int hashCode() { 1115 return mInfo.hashCode(); 1116 } 1117 } 1118 1119 /** 1120 * Shadow of AccessibilityAction. 1121 */ 1122 @Implements(value = AccessibilityNodeInfo.AccessibilityAction.class, minSdk = LOLLIPOP) 1123 public static final class ShadowAccessibilityAction { 1124 private int id; 1125 private CharSequence label; 1126 1127 @Implementation 1128 public void __constructor__(int id, CharSequence label) { 1129 if (((id & (int)ReflectionHelpers.getStaticField(AccessibilityNodeInfo.class, "ACTION_TYPE_MASK")) == 0) && Integer.bitCount(id) != 1) { 1130 throw new IllegalArgumentException("Invalid standard action id"); 1131 } 1132 this.id = id; 1133 this.label = label; 1134 } 1135 1136 @Implementation 1137 public int getId() { 1138 return id; 1139 } 1140 1141 @Implementation 1142 public CharSequence getLabel() { 1143 return label; 1144 } 1145 1146 @Override 1147 @Implementation 1148 @SuppressWarnings("EqualsHashCode") 1149 public boolean equals(Object other) { 1150 if (other == null) { 1151 return false; 1152 } 1153 1154 if (other == this) { 1155 return true; 1156 } 1157 1158 if (other.getClass() != AccessibilityAction.class) { 1159 return false; 1160 } 1161 1162 return id == ((AccessibilityAction) other).getId(); 1163 } 1164 1165 @Override 1166 public String toString() { 1167 String actionSybolicName = ReflectionHelpers.callStaticMethod( 1168 AccessibilityNodeInfo.class, "getActionSymbolicName", ClassParameter.from(int.class, id)); 1169 return "AccessibilityAction: " + actionSybolicName + " - " + label; 1170 } 1171 } 1172 1173 @Implementation 1174 public int describeContents() { 1175 return 0; 1176 } 1177 1178 @Implementation 1179 public void writeToParcel(Parcel dest, int flags) { 1180 StrictEqualityNodeWrapper wrapper = new StrictEqualityNodeWrapper(realAccessibilityNodeInfo); 1181 int keyOfWrapper = -1; 1182 for (int i = 0; i < orderedInstances.size(); i++) { 1183 if (orderedInstances.valueAt(i).equals(wrapper)) { 1184 keyOfWrapper = orderedInstances.keyAt(i); 1185 break; 1186 } 1187 } 1188 dest.writeInt(keyOfWrapper); 1189 } 1190 1191 private static int getActionTypeMaskFromFramework() { 1192 // Get the mask to determine whether an int is a legit ID for an action, defined by Android 1193 return (int)ReflectionHelpers.getStaticField(AccessibilityNodeInfo.class, "ACTION_TYPE_MASK"); 1194 } 1195 1196 private static AccessibilityAction getActionFromIdFromFrameWork(int id) { 1197 // Convert an action ID to Android standard Accessibility Action defined by Android 1198 return ReflectionHelpers.callStaticMethod( 1199 AccessibilityNodeInfo.class, "getActionSingleton", ClassParameter.from(int.class, id)); 1200 } 1201 1202 private static int getLastLegacyActionFromFrameWork() { 1203 return (int)ReflectionHelpers.getStaticField(AccessibilityNodeInfo.class, "LAST_LEGACY_STANDARD_ACTION"); 1204 } 1205 1206 /** 1207 * Configure the return result of an action if it is performed 1208 * 1209 * @param listener The listener. 1210 */ 1211 public void setOnPerformActionListener(OnPerformActionListener listener) { 1212 actionListener = listener; 1213 } 1214 1215 public interface OnPerformActionListener { 1216 boolean onPerformAccessibilityAction(int action, Bundle arguments); 1217 } 1218 } 1219