blob: dd211ac39eab07bd2c207204e3378fd2310f1b5d [file] [log] [blame]
package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import android.graphics.Rect;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityWindowInfo;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.util.ReflectionHelpers;
/**
* Shadow of {@link android.view.accessibility.AccessibilityWindowInfo} that allows a test to set
* properties that are locked in the original class.
*/
@Implements(value = AccessibilityWindowInfo.class, minSdk = LOLLIPOP)
public class ShadowAccessibilityWindowInfo {
private static final Map<StrictEqualityWindowWrapper, StackTraceElement[]> obtainedInstances =
new HashMap<>();
private List<AccessibilityWindowInfo> children = null;
private AccessibilityWindowInfo parent = null;
private AccessibilityNodeInfo rootNode = null;
private Rect boundsInScreen = new Rect();
private int type = AccessibilityWindowInfo.TYPE_APPLICATION;
private int layer = 0;
private int id = 0;
private boolean isAccessibilityFocused = false;
private boolean isActive = false;
private boolean isFocused = false;
@RealObject
private AccessibilityWindowInfo mRealAccessibilityWindowInfo;
@Implementation
public void __constructor__() {}
@Implementation
public static AccessibilityWindowInfo obtain() {
final AccessibilityWindowInfo obtainedInstance =
ReflectionHelpers.callConstructor(AccessibilityWindowInfo.class);
StrictEqualityWindowWrapper wrapper = new StrictEqualityWindowWrapper(obtainedInstance);
obtainedInstances.put(wrapper, Thread.currentThread().getStackTrace());
return obtainedInstance;
}
@Implementation
public static AccessibilityWindowInfo obtain(AccessibilityWindowInfo window) {
final ShadowAccessibilityWindowInfo shadowInfo = Shadow.extract(window);
final AccessibilityWindowInfo obtainedInstance = shadowInfo.getClone();
StrictEqualityWindowWrapper wrapper = new StrictEqualityWindowWrapper(obtainedInstance);
obtainedInstances.put(wrapper, Thread.currentThread().getStackTrace());
return obtainedInstance;
}
private AccessibilityWindowInfo getClone() {
final AccessibilityWindowInfo newInfo =
ReflectionHelpers.callConstructor(AccessibilityWindowInfo.class);
final ShadowAccessibilityWindowInfo newShadow = Shadow.extract(newInfo);
newShadow.boundsInScreen = new Rect(boundsInScreen);
newShadow.parent = parent;
newShadow.rootNode = rootNode;
newShadow.type = type;
newShadow.layer = layer;
newShadow.id = id;
newShadow.isAccessibilityFocused = isAccessibilityFocused;
newShadow.isActive = isActive;
newShadow.isFocused = isFocused;
return newInfo;
}
/**
* Clear list of obtained instance objects. {@code areThereUnrecycledWindows} will always
* return false if called immediately afterwards.
*/
public static void resetObtainedInstances() {
obtainedInstances.clear();
}
/**
* Check for leaked objects that were {@code obtain}ed but never
* {@code recycle}d.
*
* @param printUnrecycledWindowsToSystemErr - 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 windows
*/
public static boolean areThereUnrecycledWindows(boolean printUnrecycledWindowsToSystemErr) {
if (printUnrecycledWindowsToSystemErr) {
for (final StrictEqualityWindowWrapper wrapper : obtainedInstances.keySet()) {
final ShadowAccessibilityWindowInfo shadow = Shadow.extract(wrapper.mInfo);
System.err.println(String.format(
"Leaked type = %d, id = %d. Stack trace:", shadow.getType(), shadow.getId()));
for (final StackTraceElement stackTraceElement : obtainedInstances.get(wrapper)) {
System.err.println(stackTraceElement.toString());
}
}
}
return (obtainedInstances.size() != 0);
}
@Override
@Implementation
@SuppressWarnings("ReferenceEquality")
public boolean equals(Object object) {
if (!(object instanceof AccessibilityWindowInfo)) {
return false;
}
final AccessibilityWindowInfo window = (AccessibilityWindowInfo) object;
final ShadowAccessibilityWindowInfo otherShadow = Shadow.extract(window);
boolean areEqual = (type == otherShadow.getType());
areEqual &= (parent == otherShadow.getParent());
areEqual &= (rootNode == otherShadow.getRoot());
areEqual &= (layer == otherShadow.getLayer());
areEqual &= (id == otherShadow.getId());
areEqual &= (isAccessibilityFocused == otherShadow.isAccessibilityFocused());
areEqual &= (isActive == otherShadow.isActive());
areEqual &= (isFocused == otherShadow.isFocused());
Rect anotherBounds = new Rect();
otherShadow.getBoundsInScreen(anotherBounds);
areEqual &= (boundsInScreen.equals(anotherBounds));
return areEqual;
}
@Override
@Implementation
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;
}
@Implementation
public int getType() {
return type;
}
@Implementation
public int getChildCount() {
if (children == null) {
return 0;
}
return children.size();
}
@Implementation
public AccessibilityWindowInfo getChild(int index) {
if (children == null) {
return null;
}
return children.get(index);
}
@Implementation
public AccessibilityWindowInfo getParent() {
return parent;
}
@Implementation
public AccessibilityNodeInfo getRoot() {
return (rootNode == null) ? null : AccessibilityNodeInfo.obtain(rootNode);
}
@Implementation
public boolean isActive() {
return isActive;
}
@Implementation
public int getId() {
return id;
}
@Implementation
public void getBoundsInScreen(Rect outBounds) {
if (boundsInScreen == null) {
outBounds.setEmpty();
} else {
outBounds.set(boundsInScreen);
}
}
@Implementation
public int getLayer() {
return layer;
}
@Implementation
public boolean isFocused() {
return isFocused;
}
@Implementation
public boolean isAccessibilityFocused() {
return isAccessibilityFocused;
}
@Implementation
public void recycle() {
// This shadow does not track recycling of windows.
}
public void setRoot(AccessibilityNodeInfo root) {
rootNode = root;
}
public void setType(int value) {
type = value;
}
public void setBoundsInScreen(Rect bounds) {
boundsInScreen.set(bounds);
}
public void setAccessibilityFocused(boolean value) {
isAccessibilityFocused = value;
}
public void setActive(boolean value) {
isActive = value;
}
public void setId(int value) {
id = value;
}
public void setLayer(int value) {
layer = value;
}
public void setFocused(boolean focused) {
isFocused = focused;
}
public void addChild(AccessibilityWindowInfo child) {
if (children == null) {
children = new ArrayList<>();
}
children.add(child);
((ShadowAccessibilityWindowInfo) Shadow.extract(child)).parent =
mRealAccessibilityWindowInfo;
}
/**
* Private class to keep different windows referring to the same window straight
* in the mObtainedInstances map.
*/
private static class StrictEqualityWindowWrapper {
public final AccessibilityWindowInfo mInfo;
public StrictEqualityWindowWrapper(AccessibilityWindowInfo info) {
mInfo = info;
}
@Override
@SuppressWarnings("ReferenceEquality")
public boolean equals(Object object) {
if (object == null) {
return false;
}
final StrictEqualityWindowWrapper wrapper = (StrictEqualityWindowWrapper) object;
return mInfo == wrapper.mInfo;
}
@Override
public int hashCode() {
return mInfo.hashCode();
}
}
}