blob: db94550551d5ed0f074204ab2b2d5402833718b3 [file] [log] [blame]
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);
}
}