1 package org.robolectric.shadows; 2 3 import static android.os.Build.VERSION_CODES.LOLLIPOP; 4 5 import android.graphics.Rect; 6 import android.view.accessibility.AccessibilityNodeInfo; 7 import android.view.accessibility.AccessibilityWindowInfo; 8 import java.util.ArrayList; 9 import java.util.HashMap; 10 import java.util.List; 11 import java.util.Map; 12 import org.robolectric.annotation.Implementation; 13 import org.robolectric.annotation.Implements; 14 import org.robolectric.annotation.RealObject; 15 import org.robolectric.shadow.api.Shadow; 16 import org.robolectric.util.ReflectionHelpers; 17 18 /** 19 * Shadow of {@link android.view.accessibility.AccessibilityWindowInfo} that allows a test to set 20 * properties that are locked in the original class. 21 */ 22 @Implements(value = AccessibilityWindowInfo.class, minSdk = LOLLIPOP) 23 public class ShadowAccessibilityWindowInfo { 24 25 private static final Map<StrictEqualityWindowWrapper, StackTraceElement[]> obtainedInstances = 26 new HashMap<>(); 27 28 private List<AccessibilityWindowInfo> children = null; 29 30 private AccessibilityWindowInfo parent = null; 31 32 private AccessibilityNodeInfo rootNode = null; 33 34 private Rect boundsInScreen = new Rect(); 35 36 private int type = AccessibilityWindowInfo.TYPE_APPLICATION; 37 38 private int layer = 0; 39 40 private int id = 0; 41 42 private boolean isAccessibilityFocused = false; 43 44 private boolean isActive = false; 45 46 private boolean isFocused = false; 47 48 @RealObject 49 private AccessibilityWindowInfo mRealAccessibilityWindowInfo; 50 51 @Implementation 52 public void __constructor__() {} 53 54 @Implementation 55 public static AccessibilityWindowInfo obtain() { 56 final AccessibilityWindowInfo obtainedInstance = 57 ReflectionHelpers.callConstructor(AccessibilityWindowInfo.class); 58 StrictEqualityWindowWrapper wrapper = new StrictEqualityWindowWrapper(obtainedInstance); 59 obtainedInstances.put(wrapper, Thread.currentThread().getStackTrace()); 60 return obtainedInstance; 61 } 62 63 @Implementation 64 public static AccessibilityWindowInfo obtain(AccessibilityWindowInfo window) { 65 final ShadowAccessibilityWindowInfo shadowInfo = Shadow.extract(window); 66 final AccessibilityWindowInfo obtainedInstance = shadowInfo.getClone(); 67 StrictEqualityWindowWrapper wrapper = new StrictEqualityWindowWrapper(obtainedInstance); 68 obtainedInstances.put(wrapper, Thread.currentThread().getStackTrace()); 69 return obtainedInstance; 70 } 71 72 private AccessibilityWindowInfo getClone() { 73 final AccessibilityWindowInfo newInfo = 74 ReflectionHelpers.callConstructor(AccessibilityWindowInfo.class); 75 final ShadowAccessibilityWindowInfo newShadow = Shadow.extract(newInfo); 76 77 newShadow.boundsInScreen = new Rect(boundsInScreen); 78 newShadow.parent = parent; 79 newShadow.rootNode = rootNode; 80 newShadow.type = type; 81 newShadow.layer = layer; 82 newShadow.id = id; 83 newShadow.isAccessibilityFocused = isAccessibilityFocused; 84 newShadow.isActive = isActive; 85 newShadow.isFocused = isFocused; 86 87 return newInfo; 88 } 89 90 /** 91 * Clear list of obtained instance objects. {@code areThereUnrecycledWindows} will always 92 * return false if called immediately afterwards. 93 */ 94 public static void resetObtainedInstances() { 95 obtainedInstances.clear(); 96 } 97 98 /** 99 * Check for leaked objects that were {@code obtain}ed but never 100 * {@code recycle}d. 101 * 102 * @param printUnrecycledWindowsToSystemErr - if true, stack traces of calls 103 * to {@code obtain} that lack matching calls to {@code recycle} are 104 * dumped to System.err. 105 * @return {@code true} if there are unrecycled windows 106 */ 107 public static boolean areThereUnrecycledWindows(boolean printUnrecycledWindowsToSystemErr) { 108 if (printUnrecycledWindowsToSystemErr) { 109 for (final StrictEqualityWindowWrapper wrapper : obtainedInstances.keySet()) { 110 final ShadowAccessibilityWindowInfo shadow = Shadow.extract(wrapper.mInfo); 111 112 System.err.println(String.format( 113 "Leaked type = %d, id = %d. Stack trace:", shadow.getType(), shadow.getId())); 114 for (final StackTraceElement stackTraceElement : obtainedInstances.get(wrapper)) { 115 System.err.println(stackTraceElement.toString()); 116 } 117 } 118 } 119 120 return (obtainedInstances.size() != 0); 121 } 122 123 @Override 124 @Implementation 125 @SuppressWarnings("ReferenceEquality") 126 public boolean equals(Object object) { 127 if (!(object instanceof AccessibilityWindowInfo)) { 128 return false; 129 } 130 131 final AccessibilityWindowInfo window = (AccessibilityWindowInfo) object; 132 final ShadowAccessibilityWindowInfo otherShadow = Shadow.extract(window); 133 134 boolean areEqual = (type == otherShadow.getType()); 135 areEqual &= (parent == otherShadow.getParent()); 136 areEqual &= (rootNode == otherShadow.getRoot()); 137 areEqual &= (layer == otherShadow.getLayer()); 138 areEqual &= (id == otherShadow.getId()); 139 areEqual &= (isAccessibilityFocused == otherShadow.isAccessibilityFocused()); 140 areEqual &= (isActive == otherShadow.isActive()); 141 areEqual &= (isFocused == otherShadow.isFocused()); 142 Rect anotherBounds = new Rect(); 143 otherShadow.getBoundsInScreen(anotherBounds); 144 areEqual &= (boundsInScreen.equals(anotherBounds)); 145 return areEqual; 146 } 147 148 @Override 149 @Implementation 150 public int hashCode() { 151 // This is 0 for a reason. If you change it, you will break the obtained instances map in 152 // a manner that is remarkably difficult to debug. Having a dynamic hash code keeps this 153 // object from being located in the map if it was mutated after being obtained. 154 return 0; 155 } 156 157 @Implementation 158 public int getType() { 159 return type; 160 } 161 162 @Implementation 163 public int getChildCount() { 164 if (children == null) { 165 return 0; 166 } 167 168 return children.size(); 169 } 170 171 @Implementation 172 public AccessibilityWindowInfo getChild(int index) { 173 if (children == null) { 174 return null; 175 } 176 177 return children.get(index); 178 } 179 180 @Implementation 181 public AccessibilityWindowInfo getParent() { 182 return parent; 183 } 184 185 @Implementation 186 public AccessibilityNodeInfo getRoot() { 187 return (rootNode == null) ? null : AccessibilityNodeInfo.obtain(rootNode); 188 } 189 190 @Implementation 191 public boolean isActive() { 192 return isActive; 193 } 194 195 @Implementation 196 public int getId() { 197 return id; 198 } 199 200 @Implementation 201 public void getBoundsInScreen(Rect outBounds) { 202 if (boundsInScreen == null) { 203 outBounds.setEmpty(); 204 } else { 205 outBounds.set(boundsInScreen); 206 } 207 } 208 209 @Implementation 210 public int getLayer() { 211 return layer; 212 } 213 214 @Implementation 215 public boolean isFocused() { 216 return isFocused; 217 } 218 219 @Implementation 220 public boolean isAccessibilityFocused() { 221 return isAccessibilityFocused; 222 } 223 224 @Implementation 225 public void recycle() { 226 // This shadow does not track recycling of windows. 227 } 228 229 public void setRoot(AccessibilityNodeInfo root) { 230 rootNode = root; 231 } 232 233 public void setType(int value) { 234 type = value; 235 } 236 237 public void setBoundsInScreen(Rect bounds) { 238 boundsInScreen.set(bounds); 239 } 240 241 public void setAccessibilityFocused(boolean value) { 242 isAccessibilityFocused = value; 243 } 244 245 public void setActive(boolean value) { 246 isActive = value; 247 } 248 249 public void setId(int value) { 250 id = value; 251 } 252 253 public void setLayer(int value) { 254 layer = value; 255 } 256 257 public void setFocused(boolean focused) { 258 isFocused = focused; 259 } 260 261 public void addChild(AccessibilityWindowInfo child) { 262 if (children == null) { 263 children = new ArrayList<>(); 264 } 265 266 children.add(child); 267 ((ShadowAccessibilityWindowInfo) Shadow.extract(child)).parent = 268 mRealAccessibilityWindowInfo; 269 } 270 271 /** 272 * Private class to keep different windows referring to the same window straight 273 * in the mObtainedInstances map. 274 */ 275 private static class StrictEqualityWindowWrapper { 276 public final AccessibilityWindowInfo mInfo; 277 278 public StrictEqualityWindowWrapper(AccessibilityWindowInfo info) { 279 mInfo = info; 280 } 281 282 @Override 283 @SuppressWarnings("ReferenceEquality") 284 public boolean equals(Object object) { 285 if (object == null) { 286 return false; 287 } 288 289 final StrictEqualityWindowWrapper wrapper = (StrictEqualityWindowWrapper) object; 290 return mInfo == wrapper.mInfo; 291 } 292 293 @Override 294 public int hashCode() { 295 return mInfo.hashCode(); 296 } 297 } 298 }