Home | History | Annotate | Download | only in shadows
      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