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