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