
org.robolectric.shadows.ShadowAccessibilityNodeInfo Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of framework Show documentation
Show all versions of framework Show documentation
An alternative Android testing framework.
The newest version!
package org.robolectric.shadows;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
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 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;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
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;
/**
* 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 obtainedInstances =
new HashMap<>();
private static final SparseArray orderedInstances = new SparseArray<>();
// Bitmasks for actions
public static final int UNDEFINED_SELECTION_INDEX = -1;
public static final Parcelable.Creator CREATOR =
new Parcelable.Creator() {
@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
private List children;
private Rect boundsInScreen = new Rect();
private Rect boundsInParent = new Rect();
private List> 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 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;
private boolean visitedWhenCheckingChildren = false;
@RealObject
private AccessibilityNodeInfo realAccessibilityNodeInfo;
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++;
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 LinkedList<>();
shadowObtained.view = view;
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 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 LinkedList<>();
}
performedActionAndArgsList.add(new Pair(new Integer(action), arguments));
return (actionListener != null) ? actionListener.onPerformAccessibilityAction(action, arguments)
: true;
}
private boolean childrenEqualityCheck(
ShadowAccessibilityNodeInfo otherShadow,
LinkedList visitedNodes) {
if (children.size() != otherShadow.children.size()) {
return false;
}
boolean childrenEquality = true;
for (int i = 0; i < children.size(); i++) {
AccessibilityNodeInfo child = children.get(i);
ShadowAccessibilityNodeInfo childShadow = shadowOf(child);
if (!childShadow.visitedWhenCheckingChildren) {
AccessibilityNodeInfo otherChild = otherShadow.children.get(i);
visitedNodes.add(childShadow);
childShadow.visitedWhenCheckingChildren = true;
if (!child.equals(otherChild)) {
childrenEquality = false;
break;
}
childrenEquality = childShadow.childrenEqualityCheck(shadowOf(otherChild), visitedNodes);
}
}
return childrenEquality;
}
/**
* Equality check based on reference equality for mParent and mView and
* value equality for other fields.
*/
@Implementation
@Override
public boolean equals(Object object) {
if (!(object instanceof AccessibilityNodeInfo)) {
return false;
}
final AccessibilityNodeInfo info = (AccessibilityNodeInfo) object;
final ShadowAccessibilityNodeInfo otherShadow = shadowOf(info);
boolean areEqual = true;
if (children == null) {
areEqual &= (otherShadow.children == null);
} else {
LinkedList visitedNodes = new LinkedList<>();
areEqual &=
(otherShadow.children != null) && childrenEqualityCheck(otherShadow, visitedNodes);
if (parent == null) {
areEqual &= (otherShadow.parent == null);
} else if (!shadowOf(parent).visitedWhenCheckingChildren){
areEqual &= (shadowOf(parent).equals(shadowOf(otherShadow.parent)));
}
while (!visitedNodes.isEmpty()) {
ShadowAccessibilityNodeInfo visitedNode = visitedNodes.remove();
visitedNode.visitedWhenCheckingChildren = false;
}
}
areEqual &= (propertyFlags == otherShadow.propertyFlags);
if (getApiLevel() >= LOLLIPOP) {
boolean actionsArrayEquality = false;
if (actionsArray == null && otherShadow.actionsArray == null) {
actionsArrayEquality = true;
} else if (actionsArray == null || otherShadow.actionsArray == null) {
actionsArrayEquality = false;
} else {
actionsArrayEquality = actionsArray.equals(otherShadow.actionsArray);
}
areEqual &= actionsArrayEquality;
if (accessibilityWindowInfo == null) {
areEqual &= (otherShadow.accessibilityWindowInfo == null);
} else {
areEqual &= (otherShadow.accessibilityWindowInfo != null)
&& accessibilityWindowInfo.equals(otherShadow.accessibilityWindowInfo);
}
} else {
areEqual &= (actionsMask == otherShadow.actionsMask);
}
/*
* These checks have the potential to become infinite loops if there are
* loops in the labelFor or labeledBy logic. Rather than deal with this
* complexity, allow the failure since it will indicate a problem that
* needs addressing.
*/
if (labelFor == null) {
areEqual &= (otherShadow.labelFor == null);
} else {
areEqual &= (labelFor.equals(otherShadow.labelFor));
}
if (labeledBy == null) {
areEqual &= (otherShadow.labeledBy == null);
} else {
areEqual &= (labeledBy.equals(otherShadow.labeledBy));
}
areEqual &= boundsInScreen.equals(otherShadow.boundsInScreen);
areEqual &= (TextUtils.equals(contentDescription, otherShadow.contentDescription));
areEqual &= (TextUtils.equals(text, otherShadow.text));
areEqual &= TextUtils.equals(className, otherShadow.className);
areEqual &= (view == otherShadow.view);
areEqual &= (textSelectionStart == otherShadow.textSelectionStart);
areEqual &= (textSelectionEnd == otherShadow.textSelectionEnd);
areEqual &= (refreshReturnValue == otherShadow.refreshReturnValue);
areEqual &= (movementGranularities == otherShadow.movementGranularities);
areEqual &= (TextUtils.isEmpty(packageName) == TextUtils.isEmpty(otherShadow.packageName));
if (!TextUtils.isEmpty(packageName)) {
areEqual &= (packageName.toString().equals(otherShadow.packageName.toString()));
}
if (getApiLevel() >= JELLY_BEAN_MR2) {
areEqual &= TextUtils.equals(viewIdResourceName, otherShadow.viewIdResourceName);
}
if (getApiLevel() >= KITKAT) {
if (collectionInfo == null) {
areEqual &= (otherShadow.collectionInfo == null);
} else {
areEqual &= (collectionInfo.equals(otherShadow.collectionInfo));
}
if (collectionItemInfo == null) {
areEqual &= (otherShadow.collectionItemInfo == null);
} else {
areEqual &= (collectionItemInfo.equals(otherShadow.collectionItemInfo));
}
areEqual &= (inputType == otherShadow.inputType);
areEqual &= (liveRegion == otherShadow.liveRegion);
if (rangeInfo == null) {
areEqual &= (otherShadow.rangeInfo == null);
} else {
areEqual &= (rangeInfo.equals(otherShadow.rangeInfo));
}
}
if (getApiLevel() >= LOLLIPOP) {
areEqual &= (maxTextLength == otherShadow.maxTextLength);
areEqual &= TextUtils.equals(error, otherShadow.error);
}
if (getApiLevel() >= LOLLIPOP_MR1) {
if (traversalAfter == null) {
areEqual &= (otherShadow.traversalAfter == null);
} else {
areEqual &= (traversalAfter.equals(otherShadow.traversalAfter));
}
if (traversalBefore == null) {
areEqual &= (otherShadow.traversalBefore == null);
} else {
areEqual &= (traversalBefore.equals(otherShadow.traversalBefore));
}
}
return areEqual;
}
@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 LinkedList<>();
}
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 getPerformedActions() {
if (performedActionAndArgsList == null) {
performedActionAndArgsList = new LinkedList<>();
}
// Here we take the actions out of the pairs and stick them into a separate LinkedList to return
List actionsOnly = new LinkedList();
Iterator> 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> getPerformedActionsWithArgs() {
if (performedActionAndArgsList == null) {
performedActionAndArgsList = new LinkedList<>();
}
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.boundsInScreen = new Rect(boundsInScreen);
newShadow.propertyFlags = propertyFlags;
newShadow.contentDescription = contentDescription;
newShadow.text = text;
newShadow.performedActionAndArgsList = performedActionAndArgsList;
newShadow.parent = parent;
newShadow.className = className;
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 LinkedList<>();
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
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;
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
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);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy