| package org.robolectric.shadows; |
| |
| import android.os.Parcel; |
| import android.os.Parcelable; |
| import android.text.TextUtils; |
| import android.util.SparseArray; |
| import android.view.accessibility.AccessibilityEvent; |
| |
| 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.Map; |
| |
| /** |
| * Shadow of {@link android.view.accessibility.AccessibilityEvent}. |
| */ |
| @Implements(AccessibilityEvent.class) |
| public class ShadowAccessibilityEvent { |
| // Map of obtained instances of the class along with stack traces of how they were obtained |
| private static final Map<StrictEqualityEventWrapper, StackTraceElement[]> obtainedInstances = |
| new HashMap<>(); |
| |
| private static final SparseArray<StrictEqualityEventWrapper> orderedInstances = new SparseArray<>(); |
| |
| private static int sAllocationCount = 0; |
| private int eventType; |
| private CharSequence contentDescription; |
| private CharSequence packageName; |
| private CharSequence className; |
| private boolean enabled; |
| |
| @RealObject |
| private AccessibilityEvent realAccessibilityEvent; |
| |
| public void __constructor__() { |
| ReflectionHelpers.setStaticField(AccessibilityEvent.class, "CREATOR", ShadowAccessibilityEvent.CREATOR); |
| } |
| |
| public static final Parcelable.Creator<AccessibilityEvent> CREATOR = |
| new Parcelable.Creator<AccessibilityEvent>() { |
| |
| @Override |
| public AccessibilityEvent createFromParcel(Parcel source) { |
| return obtain(orderedInstances.valueAt(source.readInt()).mEvent); |
| } |
| |
| @Override |
| public AccessibilityEvent[] newArray(int size) { |
| return new AccessibilityEvent[size]; |
| }}; |
| |
| @Implementation |
| public static AccessibilityEvent obtain(AccessibilityEvent event) { |
| ShadowAccessibilityEvent shadowEvent = |
| ((ShadowAccessibilityEvent) ShadowExtractor.extract(event)); |
| AccessibilityEvent obtainedInstance = shadowEvent.getClone(); |
| |
| sAllocationCount++; |
| StrictEqualityEventWrapper wrapper = new StrictEqualityEventWrapper(obtainedInstance); |
| obtainedInstances.put(wrapper, Thread.currentThread().getStackTrace()); |
| orderedInstances.put(sAllocationCount, wrapper); |
| return obtainedInstance; |
| } |
| |
| @Implementation |
| public static AccessibilityEvent obtain(int eventType) { |
| // We explicitly avoid allocating the AccessibilityEvent from the actual pool by using |
| // the private constructor. Not doing so affects test suites which use both shadow and |
| // non-shadow objects. |
| final AccessibilityEvent obtainedInstance = |
| ReflectionHelpers.callConstructor(AccessibilityEvent.class); |
| final ShadowAccessibilityEvent shadowObtained = |
| ((ShadowAccessibilityEvent) ShadowExtractor.extract(obtainedInstance)); |
| |
| sAllocationCount++; |
| StrictEqualityEventWrapper wrapper = new StrictEqualityEventWrapper(obtainedInstance); |
| obtainedInstances.put(wrapper, Thread.currentThread().getStackTrace()); |
| orderedInstances.put(sAllocationCount, wrapper); |
| shadowObtained.eventType = eventType; |
| return obtainedInstance; |
| } |
| |
| @Implementation |
| public static AccessibilityEvent obtain() { |
| return obtain(0); |
| } |
| |
| /** |
| * Check for leaked objects that were {@code obtain}ed but never {@code recycle}d. |
| * @param printUnrecycledEventsToSystemErr - 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 events |
| */ |
| public static boolean areThereUnrecycledEvents(boolean printUnrecycledEventsToSystemErr) { |
| if (printUnrecycledEventsToSystemErr) { |
| for (final StrictEqualityEventWrapper wrapper : obtainedInstances.keySet()) { |
| final ShadowAccessibilityEvent shadow = |
| ((ShadowAccessibilityEvent) ShadowExtractor.extract(wrapper.mEvent)); |
| |
| System.err.println(String.format( |
| "Leaked AccessibilityEvent. Stack trace of allocation:", |
| 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(); |
| sAllocationCount = 0; |
| } |
| |
| @Implementation |
| public void recycle() { |
| final StrictEqualityEventWrapper wrapper = |
| new StrictEqualityEventWrapper(realAccessibilityEvent); |
| if (!obtainedInstances.containsKey(wrapper)) { |
| throw new IllegalStateException(); |
| } |
| |
| 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); |
| sAllocationCount--; |
| } |
| |
| @Implementation |
| public void setEventType(int type) { |
| eventType = type; |
| } |
| |
| @Implementation |
| public int getEventType() { |
| return eventType; |
| } |
| |
| @Implementation |
| public void setContentDescription(CharSequence description) { |
| contentDescription = description; |
| } |
| |
| @Implementation |
| public CharSequence getContentDescription() { |
| return contentDescription; |
| } |
| |
| @Implementation |
| public void setPackageName(CharSequence name) { |
| packageName = name; |
| } |
| |
| @Implementation |
| public CharSequence getPackageName() { |
| return packageName; |
| } |
| |
| @Implementation |
| public void setClassName(CharSequence name) { |
| className = name; |
| } |
| |
| @Implementation |
| public CharSequence getClassName() { |
| return className; |
| } |
| |
| @Implementation |
| public void setEnabled(boolean value) { |
| enabled = value; |
| } |
| |
| @Implementation |
| public boolean isEnabled() { |
| return enabled; |
| } |
| |
| @Override |
| @Implementation |
| public boolean equals(Object object) { |
| if (!(object instanceof AccessibilityEvent)) { |
| return false; |
| } |
| |
| final AccessibilityEvent event = (AccessibilityEvent) object; |
| final ShadowAccessibilityEvent otherShadow = |
| (ShadowAccessibilityEvent) ShadowExtractor.extract(event); |
| |
| boolean areEqual = (eventType == otherShadow.eventType); |
| areEqual &= (enabled == otherShadow.enabled); |
| areEqual &= TextUtils.equals(contentDescription, otherShadow.contentDescription); |
| areEqual &= TextUtils.equals(packageName, otherShadow.packageName); |
| areEqual &= TextUtils.equals(className, otherShadow.className); |
| |
| return areEqual; |
| } |
| |
| @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; |
| } |
| |
| /** |
| * @return A shallow copy. |
| */ |
| private AccessibilityEvent getClone() { |
| // We explicitly avoid allocating the AccessibilityEvent from the actual pool by using |
| // the private constructor. Not doing so affects test suites which use both shadow and |
| // non-shadow objects. |
| final AccessibilityEvent newEvent = ReflectionHelpers.callConstructor(AccessibilityEvent.class); |
| final ShadowAccessibilityEvent newShadow = |
| (ShadowAccessibilityEvent) ShadowExtractor.extract(newEvent); |
| |
| newShadow.eventType = eventType; |
| newShadow.contentDescription = contentDescription; |
| newShadow.packageName = packageName; |
| newShadow.className = className; |
| newShadow.enabled = enabled; |
| |
| return newEvent; |
| } |
| |
| /** |
| * Private class to keep different events straight in the mObtainedInstances map. |
| */ |
| private static class StrictEqualityEventWrapper { |
| public final AccessibilityEvent mEvent; |
| |
| public StrictEqualityEventWrapper(AccessibilityEvent event) { |
| mEvent = event; |
| } |
| |
| @Override |
| public boolean equals(Object object) { |
| if (object == null) { |
| return false; |
| } |
| |
| final StrictEqualityEventWrapper wrapper = (StrictEqualityEventWrapper) object; |
| return mEvent == wrapper.mEvent; |
| } |
| |
| @Override |
| public int hashCode() { |
| return mEvent.hashCode(); |
| } |
| } |
| |
| @Implementation |
| public int describeContents() { |
| return 0; |
| } |
| |
| @Implementation |
| public void writeToParcel(Parcel dest, int flags) { |
| StrictEqualityEventWrapper wrapper = new StrictEqualityEventWrapper(realAccessibilityEvent); |
| int indexOfWrapper = -1; |
| for (int i = 0; i < orderedInstances.size(); i++) { |
| if (orderedInstances.valueAt(i).equals(wrapper)) { |
| indexOfWrapper = i; |
| break; |
| } |
| } |
| dest.writeInt(indexOfWrapper); |
| } |
| } |