| package org.robolectric.shadows; |
| |
| import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2; |
| import static android.os.Build.VERSION_CODES.KITKAT; |
| import static android.os.Build.VERSION_CODES.LOLLIPOP; |
| import static android.os.Build.VERSION_CODES.LOLLIPOP_MR1; |
| import static org.robolectric.RuntimeEnvironment.getApiLevel; |
| import static org.robolectric.Shadows.shadowOf; |
| |
| import android.graphics.Rect; |
| import android.os.Bundle; |
| import android.os.Parcel; |
| import android.os.Parcelable; |
| import android.util.Pair; |
| import android.util.SparseArray; |
| import android.view.View; |
| import android.view.accessibility.AccessibilityNodeInfo; |
| import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; |
| import android.view.accessibility.AccessibilityNodeInfo.CollectionInfo; |
| import android.view.accessibility.AccessibilityNodeInfo.CollectionItemInfo; |
| import android.view.accessibility.AccessibilityNodeInfo.RangeInfo; |
| import android.view.accessibility.AccessibilityWindowInfo; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import org.robolectric.RuntimeEnvironment; |
| import org.robolectric.annotation.Implementation; |
| import org.robolectric.annotation.Implements; |
| import org.robolectric.annotation.RealObject; |
| import org.robolectric.util.ReflectionHelpers; |
| import org.robolectric.util.ReflectionHelpers.ClassParameter; |
| |
| /** |
| * Shadow of {@link android.view.accessibility.AccessibilityNodeInfo} that allows a test to set |
| * properties that are locked in the original class. It also keeps track of calls to |
| * {@code obtain()} and {@code recycle()} to look for bugs that mismatches. |
| */ |
| @Implements(AccessibilityNodeInfo.class) |
| public class ShadowAccessibilityNodeInfo { |
| // Map of obtained instances of the class along with stack traces of how they were obtained |
| private static final Map<StrictEqualityNodeWrapper, StackTraceElement[]> obtainedInstances = |
| new HashMap<>(); |
| |
| private static final SparseArray<StrictEqualityNodeWrapper> orderedInstances = new SparseArray<>(); |
| |
| // Bitmasks for actions |
| public static final int UNDEFINED_SELECTION_INDEX = -1; |
| |
| public static final Parcelable.Creator<AccessibilityNodeInfo> CREATOR = |
| new Parcelable.Creator<AccessibilityNodeInfo>() { |
| |
| @Override |
| public AccessibilityNodeInfo createFromParcel(Parcel source) { |
| return obtain(orderedInstances.get(source.readInt()).mInfo); |
| } |
| |
| @Override |
| public AccessibilityNodeInfo[] newArray(int size) { |
| return new AccessibilityNodeInfo[size]; |
| }}; |
| |
| private static int sAllocationCount = 0; |
| |
| private static final int CLICKABLE_MASK = 0x00000001; |
| |
| private static final int LONGCLICKABLE_MASK = 0x00000002; |
| |
| private static final int FOCUSABLE_MASK = 0x00000004; |
| |
| private static final int FOCUSED_MASK = 0x00000008; |
| |
| private static final int VISIBLE_TO_USER_MASK = 0x00000010; |
| |
| private static final int SCROLLABLE_MASK = 0x00000020; |
| |
| private static final int PASTEABLE_MASK = 0x00000040; |
| |
| private static final int EDITABLE_MASK = 0x00000080; |
| |
| private static final int TEXT_SELECTION_SETABLE_MASK = 0x00000100; |
| |
| private static final int CHECKABLE_MASK = 0x00001000; //14 |
| |
| private static final int CHECKED_MASK = 0x00002000; //14 |
| |
| private static final int ENABLED_MASK = 0x00010000; //14 |
| |
| private static final int PASSWORD_MASK = 0x00040000; //14 |
| |
| private static final int SELECTED_MASK = 0x00080000; //14 |
| |
| private static final int A11YFOCUSED_MASK = 0x00000800; //16 |
| |
| private static final int MULTILINE_MASK = 0x00020000; //19 |
| |
| private static final int CONTENT_INVALID_MASK = 0x00004000; //19 |
| |
| private static final int DISMISSABLE_MASK = 0x00008000; //19 |
| |
| private static final int CAN_OPEN_POPUP_MASK = 0x00100000; //19 |
| |
| /** |
| * Uniquely identifies the origin of the AccessibilityNodeInfo for equality |
| * testing. Two instances that come from the same node info should have the |
| * same ID. |
| */ |
| private long mOriginNodeId; |
| |
| private List<AccessibilityNodeInfo> children; |
| |
| private Rect boundsInScreen = new Rect(); |
| |
| private Rect boundsInParent = new Rect(); |
| |
| private List<Pair<Integer, Bundle>> performedActionAndArgsList; |
| |
| // In API prior to 21, actions are stored in a flag, after 21 they are stored in array of |
| // AccessibilityAction so custom actions can be supported. |
| private ArrayList<AccessibilityAction> actionsArray; |
| private int actionsMask; |
| // Storage of flags |
| |
| private int propertyFlags; |
| |
| private AccessibilityNodeInfo parent; |
| |
| private AccessibilityNodeInfo labelFor; |
| |
| private AccessibilityNodeInfo labeledBy; |
| |
| private View view; |
| |
| private CharSequence contentDescription; |
| |
| private CharSequence text; |
| |
| private CharSequence className; |
| |
| private int textSelectionStart = UNDEFINED_SELECTION_INDEX; |
| |
| private int textSelectionEnd = UNDEFINED_SELECTION_INDEX; |
| |
| private boolean refreshReturnValue = true; |
| |
| private int movementGranularities; //16 |
| |
| private CharSequence packageName; //14 |
| |
| private String viewIdResourceName; //18 |
| |
| private CollectionInfo collectionInfo; //19 |
| |
| private CollectionItemInfo collectionItemInfo; //19 |
| |
| private int inputType; //19 |
| |
| private int liveRegion; //19 |
| |
| private RangeInfo rangeInfo; //19 |
| |
| private int maxTextLength; //21 |
| |
| private CharSequence error; //21 |
| |
| private AccessibilityWindowInfo accessibilityWindowInfo; |
| |
| private AccessibilityNodeInfo traversalAfter; //22 |
| |
| private AccessibilityNodeInfo traversalBefore; //22 |
| |
| private OnPerformActionListener actionListener; |
| |
| @RealObject |
| private AccessibilityNodeInfo realAccessibilityNodeInfo; |
| |
| @Implementation |
| public void __constructor__() { |
| ReflectionHelpers.setStaticField(AccessibilityNodeInfo.class, "CREATOR", ShadowAccessibilityNodeInfo.CREATOR); |
| } |
| |
| @Implementation |
| public static AccessibilityNodeInfo obtain(AccessibilityNodeInfo info) { |
| final ShadowAccessibilityNodeInfo shadowInfo = shadowOf(info); |
| final AccessibilityNodeInfo obtainedInstance = shadowInfo.getClone(); |
| |
| sAllocationCount++; |
| if (shadowInfo.mOriginNodeId == 0) { |
| shadowInfo.mOriginNodeId = sAllocationCount; |
| } |
| StrictEqualityNodeWrapper wrapper = new StrictEqualityNodeWrapper(obtainedInstance); |
| obtainedInstances.put(wrapper, Thread.currentThread().getStackTrace()); |
| orderedInstances.put(sAllocationCount, wrapper); |
| return obtainedInstance; |
| } |
| |
| @Implementation |
| public static AccessibilityNodeInfo obtain(View view) { |
| // We explicitly avoid allocating the AccessibilityNodeInfo from the actual pool by using the |
| // private constructor. Not doing so affects test suites which use both shadow and |
| // non-shadow objects. |
| final AccessibilityNodeInfo obtainedInstance = |
| ReflectionHelpers.callConstructor(AccessibilityNodeInfo.class); |
| final ShadowAccessibilityNodeInfo shadowObtained = shadowOf(obtainedInstance); |
| |
| /* |
| * We keep a separate list of actions for each object newly obtained |
| * from a view, and perform a shallow copy during getClone. That way the |
| * list of actions performed contains all actions performed on the view |
| * by the tree of nodes initialized from it. Note that initializing two |
| * nodes with the same view will not merge the two lists, as so the list |
| * of performed actions will not contain all actions performed on the |
| * underlying view. |
| */ |
| shadowObtained.performedActionAndArgsList = new ArrayList<>(); |
| |
| shadowObtained.view = view; |
| sAllocationCount++; |
| if (shadowObtained.mOriginNodeId == 0) { |
| shadowObtained.mOriginNodeId = sAllocationCount; |
| } |
| StrictEqualityNodeWrapper wrapper = new StrictEqualityNodeWrapper(obtainedInstance); |
| obtainedInstances.put(wrapper, Thread.currentThread().getStackTrace()); |
| orderedInstances.put(sAllocationCount, wrapper); |
| return obtainedInstance; |
| } |
| |
| @Implementation |
| public static AccessibilityNodeInfo obtain() { |
| return obtain(new View(RuntimeEnvironment.application.getApplicationContext())); |
| } |
| |
| @Implementation |
| public static AccessibilityNodeInfo obtain(View root, int virtualDescendantId) { |
| AccessibilityNodeInfo node = obtain(root); |
| return node; |
| } |
| |
| /** |
| * Check for leaked objects that were {@code obtain}ed but never |
| * {@code recycle}d. |
| * |
| * @param printUnrecycledNodesToSystemErr - if true, stack traces of calls |
| * to {@code obtain} that lack matching calls to {@code recycle} are |
| * dumped to System.err. |
| * @return {@code true} if there are unrecycled nodes |
| */ |
| public static boolean areThereUnrecycledNodes(boolean printUnrecycledNodesToSystemErr) { |
| if (printUnrecycledNodesToSystemErr) { |
| for (final StrictEqualityNodeWrapper wrapper : obtainedInstances.keySet()) { |
| final ShadowAccessibilityNodeInfo shadow = shadowOf(wrapper.mInfo); |
| |
| System.err.println(String.format( |
| "Leaked contentDescription = %s. Stack trace:", shadow.getContentDescription())); |
| for (final StackTraceElement stackTraceElement : obtainedInstances.get(wrapper)) { |
| System.err.println(stackTraceElement.toString()); |
| } |
| } |
| } |
| |
| return (obtainedInstances.size() != 0); |
| } |
| |
| /** |
| * Clear list of obtained instance objects. {@code areThereUnrecycledNodes} |
| * will always return false if called immediately afterwards. |
| */ |
| public static void resetObtainedInstances() { |
| obtainedInstances.clear(); |
| orderedInstances.clear(); |
| } |
| |
| @Implementation |
| public void recycle() { |
| final StrictEqualityNodeWrapper wrapper = |
| new StrictEqualityNodeWrapper(realAccessibilityNodeInfo); |
| if (!obtainedInstances.containsKey(wrapper)) { |
| throw new IllegalStateException(); |
| } |
| |
| if (labelFor != null) { |
| labelFor.recycle(); |
| } |
| |
| if (labeledBy != null) { |
| labeledBy.recycle(); |
| } |
| if (getApiLevel() >= LOLLIPOP_MR1) { |
| if (traversalAfter != null) { |
| traversalAfter.recycle(); |
| } |
| |
| if (traversalBefore != null) { |
| traversalBefore.recycle(); |
| } |
| } |
| |
| obtainedInstances.remove(wrapper); |
| int keyOfWrapper = -1; |
| for (int i = 0; i < orderedInstances.size(); i++) { |
| int key = orderedInstances.keyAt(i); |
| if (orderedInstances.get(key).equals(wrapper)) { |
| keyOfWrapper = key; |
| break; |
| } |
| } |
| orderedInstances.remove(keyOfWrapper); |
| } |
| |
| @Implementation |
| public int getChildCount() { |
| if (children == null) { |
| return 0; |
| } |
| |
| return children.size(); |
| } |
| |
| @Implementation |
| public AccessibilityNodeInfo getChild(int index) { |
| if (children == null) { |
| return null; |
| } |
| |
| final AccessibilityNodeInfo child = children.get(index); |
| if (child == null) { |
| return null; |
| } |
| |
| return obtain(child); |
| } |
| |
| @Implementation |
| public AccessibilityNodeInfo getParent() { |
| if (parent == null) { |
| return null; |
| } |
| |
| return obtain(parent); |
| } |
| |
| @Implementation |
| public boolean refresh() { |
| return refreshReturnValue; |
| } |
| |
| public void setRefreshReturnValue(boolean refreshReturnValue) { |
| this.refreshReturnValue = refreshReturnValue; |
| } |
| |
| @Implementation |
| public boolean isClickable() { |
| return ((propertyFlags & CLICKABLE_MASK) != 0); |
| } |
| |
| @Implementation |
| public boolean isLongClickable() { |
| return ((propertyFlags & LONGCLICKABLE_MASK) != 0); |
| } |
| |
| @Implementation |
| public boolean isFocusable() { |
| return ((propertyFlags & FOCUSABLE_MASK) != 0); |
| } |
| |
| @Implementation |
| public boolean isFocused() { |
| return ((propertyFlags & FOCUSED_MASK) != 0); |
| } |
| |
| @Implementation |
| public boolean isVisibleToUser() { |
| return ((propertyFlags & VISIBLE_TO_USER_MASK) != 0); |
| } |
| |
| @Implementation |
| public boolean isScrollable() { |
| return ((propertyFlags & SCROLLABLE_MASK) != 0); |
| } |
| |
| public boolean isPasteable() { |
| return ((propertyFlags & PASTEABLE_MASK) != 0); |
| } |
| |
| @Implementation |
| public boolean isEditable() { |
| return ((propertyFlags & EDITABLE_MASK) != 0); |
| } |
| |
| public boolean isTextSelectionSetable() { |
| return ((propertyFlags & TEXT_SELECTION_SETABLE_MASK) != 0); |
| } |
| |
| @Implementation |
| public boolean isCheckable() { |
| return ((propertyFlags & CHECKABLE_MASK) != 0); |
| } |
| |
| @Implementation |
| public void setCheckable(boolean checkable) { |
| propertyFlags = (propertyFlags & ~CHECKABLE_MASK) | |
| (checkable ? CHECKABLE_MASK : 0); |
| } |
| |
| @Implementation |
| public void setChecked(boolean checked) { |
| propertyFlags = (propertyFlags & ~CHECKED_MASK) | |
| (checked ? CHECKED_MASK : 0); |
| } |
| |
| @Implementation |
| public boolean isChecked() { |
| return ((propertyFlags & CHECKED_MASK) != 0); |
| } |
| |
| @Implementation |
| public void setEnabled(boolean enabled) { |
| propertyFlags = (propertyFlags & ~ENABLED_MASK) | |
| (enabled ? ENABLED_MASK : 0); |
| } |
| |
| @Implementation |
| public boolean isEnabled() { |
| return ((propertyFlags & ENABLED_MASK) != 0); |
| } |
| |
| @Implementation |
| public void setPassword(boolean password) { |
| propertyFlags = (propertyFlags & ~PASSWORD_MASK) | |
| (password ? PASSWORD_MASK : 0); |
| } |
| |
| @Implementation |
| public boolean isPassword() { |
| return ((propertyFlags & PASSWORD_MASK) != 0); |
| } |
| |
| @Implementation |
| public void setSelected(boolean selected) { |
| propertyFlags = (propertyFlags & ~SELECTED_MASK) | |
| (selected ? SELECTED_MASK : 0); |
| } |
| |
| @Implementation |
| public boolean isSelected() { |
| return ((propertyFlags & SELECTED_MASK) != 0); |
| } |
| |
| @Implementation |
| public void setAccessibilityFocused(boolean focused) { |
| propertyFlags = (propertyFlags & ~A11YFOCUSED_MASK) | |
| (focused ? A11YFOCUSED_MASK : 0); |
| } |
| |
| @Implementation |
| public boolean isAccessibilityFocused() { |
| return ((propertyFlags & A11YFOCUSED_MASK) != 0); |
| } |
| |
| @Implementation(minSdk = LOLLIPOP) |
| public void setMultiLine(boolean multiLine) { |
| propertyFlags = (propertyFlags & ~MULTILINE_MASK) | |
| (multiLine ? MULTILINE_MASK : 0); |
| } |
| |
| @Implementation(minSdk = LOLLIPOP) |
| public boolean isMultiLine() { |
| return ((propertyFlags & MULTILINE_MASK) != 0); |
| } |
| |
| @Implementation(minSdk = LOLLIPOP) |
| public void setContentInvalid(boolean contentInvalid) { |
| propertyFlags = (propertyFlags & ~CONTENT_INVALID_MASK) | |
| (contentInvalid ? CONTENT_INVALID_MASK : 0); |
| } |
| |
| @Implementation(minSdk = LOLLIPOP) |
| public boolean isContentInvalid() { |
| return ((propertyFlags & CONTENT_INVALID_MASK) != 0); |
| } |
| |
| @Implementation(minSdk = LOLLIPOP) |
| public void setDismissable(boolean dismissable) { |
| propertyFlags = (propertyFlags & ~DISMISSABLE_MASK) | |
| (dismissable ? DISMISSABLE_MASK : 0); |
| } |
| |
| @Implementation(minSdk = LOLLIPOP) |
| public boolean isDismissable() { |
| return ((propertyFlags & DISMISSABLE_MASK) != 0); |
| } |
| |
| @Implementation(minSdk = LOLLIPOP) |
| public void setCanOpenPopup(boolean opensPopup) { |
| propertyFlags = (propertyFlags & ~CAN_OPEN_POPUP_MASK) | |
| (opensPopup ? CAN_OPEN_POPUP_MASK : 0); |
| } |
| |
| @Implementation(minSdk = LOLLIPOP) |
| public boolean canOpenPopup() { |
| return ((propertyFlags & CAN_OPEN_POPUP_MASK) != 0); |
| } |
| |
| public void setTextSelectionSetable(boolean isTextSelectionSetable) { |
| propertyFlags = (propertyFlags & ~TEXT_SELECTION_SETABLE_MASK) | |
| (isTextSelectionSetable ? TEXT_SELECTION_SETABLE_MASK : 0); |
| } |
| |
| @Implementation |
| public void setClickable(boolean isClickable) { |
| propertyFlags = (propertyFlags & ~CLICKABLE_MASK) | (isClickable ? CLICKABLE_MASK : 0); |
| } |
| |
| @Implementation |
| public void setLongClickable(boolean isLongClickable) { |
| propertyFlags = |
| (propertyFlags & ~LONGCLICKABLE_MASK) | (isLongClickable ? LONGCLICKABLE_MASK : 0); |
| } |
| |
| @Implementation |
| public void setFocusable(boolean isFocusable) { |
| propertyFlags = (propertyFlags & ~FOCUSABLE_MASK) | (isFocusable ? FOCUSABLE_MASK : 0); |
| } |
| |
| @Implementation |
| public void setFocused(boolean isFocused) { |
| propertyFlags = (propertyFlags & ~FOCUSED_MASK) | (isFocused ? FOCUSED_MASK : 0); |
| } |
| |
| @Implementation |
| public void setScrollable(boolean isScrollable) { |
| propertyFlags = (propertyFlags & ~SCROLLABLE_MASK) | (isScrollable ? SCROLLABLE_MASK : 0); |
| } |
| |
| public void setPasteable(boolean isPasteable) { |
| propertyFlags = (propertyFlags & ~PASTEABLE_MASK) | (isPasteable ? PASTEABLE_MASK : 0); |
| } |
| |
| @Implementation |
| public void setEditable(boolean isEditable) { |
| propertyFlags = (propertyFlags & ~EDITABLE_MASK) | (isEditable ? EDITABLE_MASK : 0); |
| } |
| |
| @Implementation |
| public void setVisibleToUser(boolean isVisibleToUser) { |
| propertyFlags = |
| (propertyFlags & ~VISIBLE_TO_USER_MASK) | (isVisibleToUser ? VISIBLE_TO_USER_MASK : 0); |
| } |
| |
| @Implementation |
| public void setContentDescription(CharSequence description) { |
| contentDescription = description; |
| } |
| |
| @Implementation |
| public CharSequence getContentDescription() { |
| return contentDescription; |
| } |
| |
| @Implementation |
| public void setClassName(CharSequence name) { |
| className = name; |
| } |
| |
| @Implementation |
| public CharSequence getClassName() { |
| return className; |
| } |
| |
| @Implementation |
| public void setText(CharSequence t) { |
| text = t; |
| } |
| |
| @Implementation |
| public CharSequence getText() { |
| return text; |
| } |
| |
| @Implementation |
| public void setTextSelection(int start, int end) { |
| textSelectionStart = start; |
| textSelectionEnd = end; |
| } |
| |
| /** |
| * Gets the text selection start. |
| * |
| * @return The text selection start if there is selection or UNDEFINED_SELECTION_INDEX. |
| */ |
| @Implementation |
| public int getTextSelectionStart() { |
| return textSelectionStart; |
| } |
| |
| /** |
| * Gets the text selection end. |
| * |
| * @return The text selection end if there is selection or UNDEFINED_SELECTION_INDEX. |
| */ |
| @Implementation |
| public int getTextSelectionEnd() { |
| return textSelectionEnd; |
| } |
| |
| @Implementation |
| public AccessibilityNodeInfo getLabelFor() { |
| if (labelFor == null) { |
| return null; |
| } |
| |
| return obtain(labelFor); |
| } |
| |
| public void setLabelFor(AccessibilityNodeInfo info) { |
| if (labelFor != null) { |
| labelFor.recycle(); |
| } |
| |
| labelFor = obtain(info); |
| } |
| |
| @Implementation |
| public AccessibilityNodeInfo getLabeledBy() { |
| if (labeledBy == null) { |
| return null; |
| } |
| |
| return obtain(labeledBy); |
| } |
| |
| public void setLabeledBy(AccessibilityNodeInfo info) { |
| if (labeledBy != null) { |
| labeledBy.recycle(); |
| } |
| |
| labeledBy = obtain(info); |
| } |
| |
| @Implementation |
| public int getMovementGranularities() { |
| return movementGranularities; |
| } |
| |
| @Implementation |
| public void setMovementGranularities(int movementGranularities) { |
| this.movementGranularities = movementGranularities; |
| } |
| |
| @Implementation |
| public CharSequence getPackageName() { |
| return packageName; |
| } |
| |
| @Implementation |
| public void setPackageName(CharSequence packageName) { |
| this.packageName = packageName; |
| } |
| |
| @Implementation(minSdk = JELLY_BEAN_MR2) |
| public String getViewIdResourceName() { |
| return viewIdResourceName; |
| } |
| |
| @Implementation(minSdk = JELLY_BEAN_MR2) |
| public void setViewIdResourceName(String viewIdResourceName) { |
| this.viewIdResourceName = viewIdResourceName; |
| } |
| |
| @Implementation(minSdk = KITKAT) |
| public CollectionInfo getCollectionInfo() { |
| return collectionInfo; |
| } |
| |
| @Implementation(minSdk = KITKAT) |
| public void setCollectionInfo(CollectionInfo collectionInfo) { |
| this.collectionInfo = collectionInfo; |
| } |
| |
| @Implementation(minSdk = KITKAT) |
| public CollectionItemInfo getCollectionItemInfo() { |
| return collectionItemInfo; |
| } |
| |
| @Implementation(minSdk = KITKAT) |
| public void setCollectionItemInfo(CollectionItemInfo collectionItemInfo) { |
| this.collectionItemInfo = collectionItemInfo; |
| } |
| |
| @Implementation(minSdk = KITKAT) |
| public int getInputType() { |
| return inputType; |
| } |
| |
| @Implementation(minSdk = KITKAT) |
| public void setInputType(int inputType) { |
| this.inputType = inputType; |
| } |
| |
| @Implementation(minSdk = KITKAT) |
| public int getLiveRegion() { |
| return liveRegion; |
| } |
| |
| @Implementation(minSdk = KITKAT) |
| public void setLiveRegion(int liveRegion) { |
| this.liveRegion = liveRegion; |
| } |
| |
| @Implementation(minSdk = KITKAT) |
| public RangeInfo getRangeInfo() { |
| return rangeInfo; |
| } |
| |
| @Implementation(minSdk = KITKAT) |
| public void setRangeInfo(RangeInfo rangeInfo) { |
| this.rangeInfo = rangeInfo; |
| } |
| |
| @Implementation(minSdk = LOLLIPOP) |
| public int getMaxTextLength() { |
| return maxTextLength; |
| } |
| |
| @Implementation(minSdk = LOLLIPOP) |
| public void setMaxTextLength(int maxTextLength) { |
| this.maxTextLength = maxTextLength; |
| } |
| |
| @Implementation(minSdk = LOLLIPOP) |
| public CharSequence getError() { |
| return error; |
| } |
| |
| @Implementation(minSdk = LOLLIPOP) |
| public void setError(CharSequence error) { |
| this.error = error; |
| } |
| |
| @Implementation(minSdk = LOLLIPOP_MR1) |
| public AccessibilityNodeInfo getTraversalAfter() { |
| if (traversalAfter == null) { |
| return null; |
| } |
| |
| return obtain(traversalAfter); |
| } |
| |
| @Implementation(minSdk = LOLLIPOP_MR1) |
| public void setTraversalAfter(AccessibilityNodeInfo info) { |
| if (this.traversalAfter != null) { |
| this.traversalAfter.recycle(); |
| } |
| |
| this.traversalAfter = obtain(info); |
| } |
| |
| @Implementation(minSdk = LOLLIPOP_MR1) |
| public AccessibilityNodeInfo getTraversalBefore() { |
| if (traversalBefore == null) { |
| return null; |
| } |
| |
| return obtain(traversalBefore); |
| } |
| |
| @Implementation(minSdk = LOLLIPOP_MR1) |
| public void setTraversalBefore(AccessibilityNodeInfo info) { |
| if (this.traversalBefore != null) { |
| this.traversalBefore.recycle(); |
| } |
| |
| this.traversalBefore = obtain(info); |
| } |
| |
| @Implementation |
| public void setSource (View source) { |
| this.view = source; |
| } |
| |
| @Implementation |
| public void setSource (View root, int virtualDescendantId) { |
| this.view = root; |
| } |
| |
| @Implementation |
| public void getBoundsInScreen(Rect outBounds) { |
| if (boundsInScreen == null) { |
| boundsInScreen = new Rect(); |
| } |
| outBounds.set(boundsInScreen); |
| } |
| |
| @Implementation |
| public void getBoundsInParent(Rect outBounds) { |
| if (boundsInParent == null) { |
| boundsInParent = new Rect(); |
| } |
| outBounds.set(boundsInParent); |
| } |
| |
| @Implementation |
| public void setBoundsInScreen(Rect b) { |
| if (boundsInScreen == null) { |
| boundsInScreen = new Rect(b); |
| } else { |
| boundsInScreen.set(b); |
| } |
| } |
| |
| @Implementation |
| public void setBoundsInParent(Rect b) { |
| if (boundsInParent == null) { |
| boundsInParent = new Rect(b); |
| } else { |
| boundsInParent.set(b); |
| } |
| } |
| |
| @Implementation |
| public void addAction(int action) { |
| if (getApiLevel() >= LOLLIPOP) { |
| if ((action & getActionTypeMaskFromFramework()) != 0) { |
| throw new IllegalArgumentException("Action is not a combination of the standard " + |
| "actions: " + action); |
| } |
| int remainingIds = action; |
| while (remainingIds > 0) { |
| final int id = 1 << Integer.numberOfTrailingZeros(remainingIds); |
| remainingIds &= ~id; |
| AccessibilityAction convertedAction = getActionFromIdFromFrameWork(id); |
| addAction(convertedAction); |
| } |
| } else { |
| actionsMask |= action; |
| } |
| } |
| |
| @Implementation(minSdk = LOLLIPOP) |
| public void addAction(AccessibilityAction action) { |
| if (action == null) { |
| return; |
| } |
| |
| if (actionsArray == null) { |
| actionsArray = new ArrayList<>(); |
| } |
| actionsArray.remove(action); |
| actionsArray.add(action); |
| } |
| |
| @Implementation(minSdk = LOLLIPOP) |
| public void removeAction(int action) { |
| AccessibilityAction convertedAction = getActionFromIdFromFrameWork(action); |
| removeAction(convertedAction); |
| } |
| |
| @Implementation(minSdk = LOLLIPOP) |
| public boolean removeAction(AccessibilityAction action) { |
| if (action == null || actionsArray == null) { |
| return false; |
| } |
| return actionsArray.remove(action); |
| } |
| |
| /** |
| * Obtain flags for actions supported. Currently only supports |
| * {@link AccessibilityNodeInfo#ACTION_CLICK}, |
| * {@link AccessibilityNodeInfo#ACTION_LONG_CLICK}, |
| * {@link AccessibilityNodeInfo#ACTION_SCROLL_FORWARD}, |
| * {@link AccessibilityNodeInfo#ACTION_PASTE}, |
| * {@link AccessibilityNodeInfo#ACTION_FOCUS}, |
| * {@link AccessibilityNodeInfo#ACTION_SET_SELECTION}, |
| * {@link AccessibilityNodeInfo#ACTION_SCROLL_BACKWARD} |
| * Returned value is derived from the getters. |
| * |
| * @return Action mask. 0 if no actions supported. |
| */ |
| @Implementation |
| public int getActions() { |
| if (getApiLevel() >= LOLLIPOP) { |
| int returnValue = 0; |
| if (actionsArray == null) { |
| return returnValue; |
| } |
| |
| // Custom actions are only returned by getActionsList |
| final int actionSize = actionsArray.size(); |
| for (int i = 0; i < actionSize; i++) { |
| int actionId = actionsArray.get(i).getId(); |
| if (actionId <= getLastLegacyActionFromFrameWork()) { |
| returnValue |= actionId; |
| } |
| } |
| return returnValue; |
| } else { |
| return actionsMask; |
| } |
| } |
| |
| @Implementation(minSdk = LOLLIPOP) |
| public AccessibilityWindowInfo getWindow() { |
| return accessibilityWindowInfo; |
| } |
| |
| public void setAccessibilityWindowInfo(AccessibilityWindowInfo info) { |
| accessibilityWindowInfo = info; |
| } |
| |
| @Implementation(minSdk = LOLLIPOP) |
| public List<AccessibilityAction> getActionList() { |
| if (actionsArray == null) { |
| return Collections.emptyList(); |
| } |
| |
| return actionsArray; |
| } |
| |
| @Implementation |
| public boolean performAction(int action) { |
| return performAction(action, null); |
| } |
| |
| @Implementation |
| public boolean performAction(int action, Bundle arguments) { |
| if (performedActionAndArgsList == null) { |
| performedActionAndArgsList = new ArrayList<>(); |
| } |
| |
| performedActionAndArgsList.add(new Pair<>(action, arguments)); |
| return actionListener == null || actionListener.onPerformAccessibilityAction(action, arguments); |
| } |
| |
| /** |
| * Equality check based on reference equality of the Views from which these instances were |
| * created, or the equality of their assigned IDs. |
| */ |
| @Implementation |
| @Override |
| public boolean equals(Object object) { |
| if (!(object instanceof AccessibilityNodeInfo)) { |
| return false; |
| } |
| |
| final AccessibilityNodeInfo info = (AccessibilityNodeInfo) object; |
| final ShadowAccessibilityNodeInfo otherShadow = shadowOf(info); |
| |
| if (this.view != null) { |
| return this.view == otherShadow.view; |
| } |
| if (this.mOriginNodeId != 0) { |
| return this.mOriginNodeId == otherShadow.mOriginNodeId; |
| } |
| throw new IllegalStateException("Node has neither an ID nor View"); |
| } |
| |
| @Implementation |
| @Override |
| public int hashCode() { |
| // This is 0 for a reason. If you change it, you will break the obtained |
| // instances map in a manner that is remarkably difficult to debug. |
| // Having a dynamic hash code keeps this object from being located |
| // in the map if it was mutated after being obtained. |
| return 0; |
| } |
| |
| /** |
| * Add a child node to this one. Also initializes the parent field of the |
| * child. |
| * |
| * @param child The node to be added as a child. |
| */ |
| public void addChild(AccessibilityNodeInfo child) { |
| if (children == null) { |
| children = new ArrayList<>(); |
| } |
| |
| children.add(child); |
| (shadowOf(child)).parent = realAccessibilityNodeInfo; |
| } |
| |
| @Implementation |
| public void addChild(View child) { |
| AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain(child); |
| addChild(node); |
| } |
| |
| @Implementation |
| public void addChild(View root, int virtualDescendantId) { |
| AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain(root, virtualDescendantId); |
| addChild(node); |
| } |
| |
| /** |
| * @return The list of arguments for the various calls to performAction. Unmodifiable. |
| */ |
| public List<Integer> getPerformedActions() { |
| if (performedActionAndArgsList == null) { |
| performedActionAndArgsList = new ArrayList<>(); |
| } |
| |
| // Here we take the actions out of the pairs and stick them into a separate LinkedList to return |
| List<Integer> actionsOnly = new ArrayList<>(); |
| Iterator<Pair<Integer, Bundle>> iter = performedActionAndArgsList.iterator(); |
| while (iter.hasNext()) { |
| actionsOnly.add(iter.next().first); |
| } |
| |
| return Collections.unmodifiableList(actionsOnly); |
| } |
| |
| /** |
| * @return The list of arguments for the various calls to performAction. Unmodifiable. |
| */ |
| public List<Pair<Integer, Bundle>> getPerformedActionsWithArgs() { |
| if (performedActionAndArgsList == null) { |
| performedActionAndArgsList = new ArrayList<>(); |
| } |
| return Collections.unmodifiableList(performedActionAndArgsList); |
| } |
| |
| /** |
| * @return A shallow copy. |
| */ |
| private AccessibilityNodeInfo getClone() { |
| // We explicitly avoid allocating the AccessibilityNodeInfo from the actual pool by using |
| // the private constructor. Not doing so affects test suites which use both shadow and |
| // non-shadow objects. |
| final AccessibilityNodeInfo newInfo = |
| ReflectionHelpers.callConstructor(AccessibilityNodeInfo.class); |
| final ShadowAccessibilityNodeInfo newShadow = shadowOf(newInfo); |
| |
| newShadow.mOriginNodeId = mOriginNodeId; |
| newShadow.boundsInScreen = new Rect(boundsInScreen); |
| newShadow.propertyFlags = propertyFlags; |
| newShadow.contentDescription = contentDescription; |
| newShadow.text = text; |
| newShadow.performedActionAndArgsList = performedActionAndArgsList; |
| newShadow.parent = parent; |
| newShadow.className = className; |
| newShadow.labelFor = labelFor; |
| newShadow.labeledBy = labeledBy; |
| newShadow.view = view; |
| newShadow.textSelectionStart = textSelectionStart; |
| newShadow.textSelectionEnd = textSelectionEnd; |
| newShadow.actionListener = actionListener; |
| if (getApiLevel() >= LOLLIPOP) { |
| if (actionsArray != null) { |
| newShadow.actionsArray = new ArrayList<>(); |
| newShadow.actionsArray.addAll(actionsArray); |
| } else { |
| newShadow.actionsArray = null; |
| } |
| } else { |
| newShadow.actionsMask = actionsMask; |
| } |
| |
| if (children != null) { |
| newShadow.children = new ArrayList<>(); |
| newShadow.children.addAll(children); |
| } else { |
| newShadow.children = null; |
| } |
| |
| newShadow.refreshReturnValue = refreshReturnValue; |
| newShadow.movementGranularities = movementGranularities; |
| newShadow.packageName = packageName; |
| if (getApiLevel() >= JELLY_BEAN_MR2) { |
| newShadow.viewIdResourceName = viewIdResourceName; |
| } |
| if (getApiLevel() >= KITKAT) { |
| newShadow.collectionInfo = collectionInfo; |
| newShadow.collectionItemInfo = collectionItemInfo; |
| newShadow.inputType = inputType; |
| newShadow.liveRegion = liveRegion; |
| newShadow.rangeInfo = rangeInfo; |
| } |
| if (getApiLevel() >= LOLLIPOP) { |
| newShadow.maxTextLength = maxTextLength; |
| newShadow.error = error; |
| } |
| if (getApiLevel() >= LOLLIPOP_MR1) { |
| newShadow.traversalAfter = (traversalAfter == null) ? null : obtain(traversalAfter); |
| newShadow.traversalBefore = (traversalBefore == null) ? null : obtain(traversalBefore); |
| } |
| return newInfo; |
| } |
| |
| /** |
| * Private class to keep different nodes referring to the same view straight |
| * in the mObtainedInstances map. |
| */ |
| private static class StrictEqualityNodeWrapper { |
| public final AccessibilityNodeInfo mInfo; |
| |
| public StrictEqualityNodeWrapper(AccessibilityNodeInfo info) { |
| mInfo = info; |
| } |
| |
| @Override |
| @SuppressWarnings("ReferenceEquality") |
| public boolean equals(Object object) { |
| if (object == null) { |
| return false; |
| } |
| |
| final StrictEqualityNodeWrapper wrapper = (StrictEqualityNodeWrapper) object; |
| return mInfo == wrapper.mInfo; |
| } |
| |
| @Override |
| public int hashCode() { |
| return mInfo.hashCode(); |
| } |
| } |
| |
| /** |
| * Shadow of AccessibilityAction. |
| */ |
| @Implements(value = AccessibilityNodeInfo.AccessibilityAction.class, minSdk = LOLLIPOP) |
| public static final class ShadowAccessibilityAction { |
| private int id; |
| private CharSequence label; |
| |
| @Implementation |
| public void __constructor__(int id, CharSequence label) { |
| if (((id & (int)ReflectionHelpers.getStaticField(AccessibilityNodeInfo.class, "ACTION_TYPE_MASK")) == 0) && Integer.bitCount(id) != 1) { |
| throw new IllegalArgumentException("Invalid standard action id"); |
| } |
| this.id = id; |
| this.label = label; |
| } |
| |
| @Implementation |
| public int getId() { |
| return id; |
| } |
| |
| @Implementation |
| public CharSequence getLabel() { |
| return label; |
| } |
| |
| @Override |
| @Implementation |
| @SuppressWarnings("EqualsHashCode") |
| public boolean equals(Object other) { |
| if (other == null) { |
| return false; |
| } |
| |
| if (other == this) { |
| return true; |
| } |
| |
| if (other.getClass() != AccessibilityAction.class) { |
| return false; |
| } |
| |
| return id == ((AccessibilityAction) other).getId(); |
| } |
| |
| @Override |
| public String toString() { |
| String actionSybolicName = ReflectionHelpers.callStaticMethod( |
| AccessibilityNodeInfo.class, "getActionSymbolicName", ClassParameter.from(int.class, id)); |
| return "AccessibilityAction: " + actionSybolicName + " - " + label; |
| } |
| } |
| |
| @Implementation |
| public int describeContents() { |
| return 0; |
| } |
| |
| @Implementation |
| public void writeToParcel(Parcel dest, int flags) { |
| StrictEqualityNodeWrapper wrapper = new StrictEqualityNodeWrapper(realAccessibilityNodeInfo); |
| int keyOfWrapper = -1; |
| for (int i = 0; i < orderedInstances.size(); i++) { |
| if (orderedInstances.valueAt(i).equals(wrapper)) { |
| keyOfWrapper = orderedInstances.keyAt(i); |
| break; |
| } |
| } |
| dest.writeInt(keyOfWrapper); |
| } |
| |
| private static int getActionTypeMaskFromFramework() { |
| // Get the mask to determine whether an int is a legit ID for an action, defined by Android |
| return (int)ReflectionHelpers.getStaticField(AccessibilityNodeInfo.class, "ACTION_TYPE_MASK"); |
| } |
| |
| private static AccessibilityAction getActionFromIdFromFrameWork(int id) { |
| // Convert an action ID to Android standard Accessibility Action defined by Android |
| return ReflectionHelpers.callStaticMethod( |
| AccessibilityNodeInfo.class, "getActionSingleton", ClassParameter.from(int.class, id)); |
| } |
| |
| private static int getLastLegacyActionFromFrameWork() { |
| return (int)ReflectionHelpers.getStaticField(AccessibilityNodeInfo.class, "LAST_LEGACY_STANDARD_ACTION"); |
| } |
| |
| /** |
| * Configure the return result of an action if it is performed |
| * |
| * @param listener The listener. |
| */ |
| public void setOnPerformActionListener(OnPerformActionListener listener) { |
| actionListener = listener; |
| } |
| |
| public interface OnPerformActionListener { |
| boolean onPerformAccessibilityAction(int action, Bundle arguments); |
| } |
| } |