package org.robolectric.shadows; import android.graphics.Rect; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityWindowInfo; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import org.robolectric.annotation.RealObject; import org.robolectric.internal.ShadowExtractor; import org.robolectric.util.ReflectionHelpers; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import static android.os.Build.VERSION_CODES.LOLLIPOP; /** * 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; 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 = ((ShadowAccessibilityWindowInfo) ShadowExtractor.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 = (ShadowAccessibilityWindowInfo) ShadowExtractor.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 = ((ShadowAccessibilityWindowInfo) ShadowExtractor.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 public boolean equals(Object object) { if (!(object instanceof AccessibilityWindowInfo)) { return false; } final AccessibilityWindowInfo window = (AccessibilityWindowInfo) object; final ShadowAccessibilityWindowInfo otherShadow = (ShadowAccessibilityWindowInfo) ShadowExtractor.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 LinkedList<>(); } children.add(child); ((ShadowAccessibilityWindowInfo) ShadowExtractor.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 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(); } } }