blob: e55f76fbc289c12e32355965a8068aac1f1ef8fb [file] [log] [blame]
package com.xtremelabs.robolectric.bytecode;
import android.net.Uri;
import com.xtremelabs.robolectric.internal.DoNotInstrument;
import com.xtremelabs.robolectric.internal.Instrument;
import javassist.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@SuppressWarnings({"UnusedDeclaration"})
public class AndroidTranslator implements Translator {
/**
* IMPORTANT -- increment this number when the bytecode generated for modified classes changes
* so the cache file can be invalidated.
*/
public static final int CACHE_VERSION = 21;
private static final List<ClassHandler> CLASS_HANDLERS = new ArrayList<ClassHandler>();
private ClassHandler classHandler;
private ClassCache classCache;
private final List<String> instrumentingList = new ArrayList<String>();
private final List<String> instrumentingExcludeList = new ArrayList<String>();
public AndroidTranslator(ClassHandler classHandler, ClassCache classCache) {
this.classHandler = classHandler;
this.classCache = classCache;
// Initialize lists
instrumentingList.add("android.");
instrumentingList.add("com.google.android.maps");
instrumentingList.add("org.apache.http.impl.client.DefaultRequestDirector");
instrumentingExcludeList.add("android.support.v4.app.NotificationCompat");
instrumentingExcludeList.add("android.support.v4.content.LocalBroadcastManager");
instrumentingExcludeList.add("android.support.v4.util.LruCache");
}
public AndroidTranslator(ClassHandler classHandler, ClassCache classCache, List<String> customShadowClassNames) {
this(classHandler, classCache);
if (customShadowClassNames != null && !customShadowClassNames.isEmpty()) {
instrumentingList.addAll(customShadowClassNames);
}
}
public void addCustomShadowClass(String customShadowClassName) {
if (!instrumentingList.contains(customShadowClassName)) {
instrumentingList.add(customShadowClassName);
}
}
public static ClassHandler getClassHandler(int index) {
return CLASS_HANDLERS.get(index);
}
@Override
public void start(ClassPool classPool) throws NotFoundException, CannotCompileException {
injectClassHandlerToInstrumentedClasses(classPool);
}
private void injectClassHandlerToInstrumentedClasses(ClassPool classPool) throws NotFoundException, CannotCompileException {
int index;
synchronized (CLASS_HANDLERS) {
CLASS_HANDLERS.add(classHandler);
index = CLASS_HANDLERS.size() - 1;
}
CtClass robolectricInternalsCtClass = classPool.get(RobolectricInternals.class.getName());
robolectricInternalsCtClass.setModifiers(Modifier.PUBLIC);
robolectricInternalsCtClass.getClassInitializer().insertBefore("{\n" +
"classHandler = " + AndroidTranslator.class.getName() + ".getClassHandler(" + index + ");\n" +
"}");
}
@Override
public void onLoad(ClassPool classPool, String className) throws NotFoundException, CannotCompileException {
if (classCache.isWriting()) {
throw new IllegalStateException("shouldn't be modifying bytecode after we've started writing cache! class=" + className);
}
if (classHasFromAndroidEquivalent(className)) {
replaceClassWithFromAndroidEquivalent(classPool, className);
return;
}
CtClass ctClass;
try {
ctClass = classPool.get(className);
} catch (NotFoundException e) {
throw new IgnorableClassNotFoundException(e);
}
if (shouldInstrument(ctClass)) {
int modifiers = ctClass.getModifiers();
if (Modifier.isFinal(modifiers)) {
ctClass.setModifiers(modifiers & ~Modifier.FINAL);
}
classHandler.instrument(ctClass);
fixConstructors(ctClass);
fixMethods(ctClass);
try {
classCache.addClass(className, ctClass.toBytecode());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
/* package */ boolean shouldInstrument(CtClass ctClass) {
if (ctClass.hasAnnotation(Instrument.class)) {
return true;
} else if (ctClass.isInterface() || ctClass.hasAnnotation(DoNotInstrument.class)) {
return false;
} else {
for (String klassName : instrumentingExcludeList) {
if (ctClass.getName().startsWith(klassName)) {
return false;
}
}
for (String klassName : instrumentingList) {
if (ctClass.getName().startsWith(klassName)) {
return true;
}
}
return false;
}
}
private boolean classHasFromAndroidEquivalent(String className) {
return className.startsWith(Uri.class.getName());
}
private void replaceClassWithFromAndroidEquivalent(ClassPool classPool, String className) throws NotFoundException {
FromAndroidClassNameParts classNameParts = new FromAndroidClassNameParts(className);
if (classNameParts.isFromAndroid()) return;
String from = classNameParts.getNameWithFromAndroid();
CtClass ctClass = classPool.getAndRename(from, className);
ClassMap map = new ClassMap() {
@Override
public Object get(Object jvmClassName) {
FromAndroidClassNameParts classNameParts = new FromAndroidClassNameParts(jvmClassName.toString());
if (classNameParts.isFromAndroid()) {
return classNameParts.getNameWithoutFromAndroid();
} else {
return jvmClassName;
}
}
};
ctClass.replaceClassName(map);
}
class FromAndroidClassNameParts {
private static final String TOKEN = "__FromAndroid";
private String prefix;
private String suffix;
FromAndroidClassNameParts(String name) {
int dollarIndex = name.indexOf("$");
prefix = name;
suffix = "";
if (dollarIndex > -1) {
prefix = name.substring(0, dollarIndex);
suffix = name.substring(dollarIndex);
}
}
public boolean isFromAndroid() {
return prefix.endsWith(TOKEN);
}
public String getNameWithFromAndroid() {
return prefix + TOKEN + suffix;
}
public String getNameWithoutFromAndroid() {
return prefix.replace(TOKEN, "") + suffix;
}
}
private void addBypassShadowField(CtClass ctClass, String fieldName) {
try {
try {
ctClass.getField(fieldName);
} catch (NotFoundException e) {
CtField field = new CtField(CtClass.booleanType, fieldName, ctClass);
field.setModifiers(java.lang.reflect.Modifier.PUBLIC | java.lang.reflect.Modifier.STATIC);
ctClass.addField(field);
}
} catch (CannotCompileException e) {
throw new RuntimeException(e);
}
}
private void fixConstructors(CtClass ctClass) throws CannotCompileException, NotFoundException {
if (ctClass.isEnum()) {
// skip enum constructors because they are not stubs in android.jar
return;
}
boolean hasDefault = false;
for (CtConstructor ctConstructor : ctClass.getDeclaredConstructors()) {
try {
fixConstructor(ctClass, hasDefault, ctConstructor);
if (ctConstructor.getParameterTypes().length == 0) {
hasDefault = true;
}
} catch (Exception e) {
throw new RuntimeException("problem instrumenting " + ctConstructor, e);
}
}
if (!hasDefault) {
String methodBody = generateConstructorBody(ctClass, new CtClass[0]);
ctClass.addConstructor(CtNewConstructor.make(new CtClass[0], new CtClass[0], "{\n" + methodBody + "}\n", ctClass));
}
}
private boolean fixConstructor(CtClass ctClass, boolean needsDefault, CtConstructor ctConstructor) throws NotFoundException, CannotCompileException {
String methodBody = generateConstructorBody(ctClass, ctConstructor.getParameterTypes());
ctConstructor.setBody("{\n" + methodBody + "}\n");
return needsDefault;
}
private String generateConstructorBody(CtClass ctClass, CtClass[] parameterTypes) throws NotFoundException {
return generateMethodBody(ctClass,
new CtMethod(CtClass.voidType, "<init>", parameterTypes, ctClass),
CtClass.voidType,
Type.VOID,
false,
false);
}
private void fixMethods(CtClass ctClass) throws NotFoundException, CannotCompileException {
for (CtMethod ctMethod : ctClass.getDeclaredMethods()) {
fixMethod(ctClass, ctMethod, true);
}
CtMethod equalsMethod = ctClass.getMethod("equals", "(Ljava/lang/Object;)Z");
CtMethod hashCodeMethod = ctClass.getMethod("hashCode", "()I");
CtMethod toStringMethod = ctClass.getMethod("toString", "()Ljava/lang/String;");
fixMethod(ctClass, equalsMethod, false);
fixMethod(ctClass, hashCodeMethod, false);
fixMethod(ctClass, toStringMethod, false);
}
private String describe(CtMethod ctMethod) throws NotFoundException {
return Modifier.toString(ctMethod.getModifiers()) + " " + ctMethod.getReturnType().getSimpleName() + " " + ctMethod.getLongName();
}
private void fixMethod(CtClass ctClass, CtMethod ctMethod, boolean wasFoundInClass) throws NotFoundException {
String describeBefore = describe(ctMethod);
try {
CtClass declaringClass = ctMethod.getDeclaringClass();
int originalModifiers = ctMethod.getModifiers();
boolean wasNative = Modifier.isNative(originalModifiers);
boolean wasFinal = Modifier.isFinal(originalModifiers);
boolean wasAbstract = Modifier.isAbstract(originalModifiers);
boolean wasDeclaredInClass = ctClass == declaringClass;
if (wasFinal && ctClass.isEnum()) {
return;
}
int newModifiers = originalModifiers;
if (wasNative) {
newModifiers = Modifier.clear(newModifiers, Modifier.NATIVE);
}
if (wasFinal) {
newModifiers = Modifier.clear(newModifiers, Modifier.FINAL);
}
if (wasFoundInClass) {
ctMethod.setModifiers(newModifiers);
}
CtClass returnCtClass = ctMethod.getReturnType();
Type returnType = Type.find(returnCtClass);
String methodName = ctMethod.getName();
CtClass[] paramTypes = ctMethod.getParameterTypes();
// if (!isAbstract) {
// if (methodName.startsWith("set") && paramTypes.length == 1) {
// String fieldName = "__" + methodName.substring(3);
// if (declareField(ctClass, fieldName, paramTypes[0])) {
// methodBody = fieldName + " = $1;\n" + methodBody;
// }
// } else if (methodName.startsWith("get") && paramTypes.length == 0) {
// String fieldName = "__" + methodName.substring(3);
// if (declareField(ctClass, fieldName, returnType)) {
// methodBody = "return " + fieldName + ";\n";
// }
// }
// }
boolean isStatic = Modifier.isStatic(originalModifiers);
String methodBody = generateMethodBody(ctClass, ctMethod, wasNative, wasAbstract, returnCtClass, returnType, isStatic, !wasFoundInClass);
if (!wasFoundInClass) {
CtMethod newMethod = makeNewMethod(ctClass, ctMethod, returnCtClass, methodName, paramTypes, "{\n" + methodBody + generateCallToSuper(methodName, paramTypes) + "\n}");
newMethod.setModifiers(newModifiers);
if (wasDeclaredInClass) {
ctMethod.insertBefore("{\n" + methodBody + "}\n");
} else {
ctClass.addMethod(newMethod);
}
} else if (wasAbstract || wasNative) {
CtMethod newMethod = makeNewMethod(ctClass, ctMethod, returnCtClass, methodName, paramTypes, "{\n" + methodBody + "\n}");
ctMethod.setBody(newMethod, null);
} else {
ctMethod.insertBefore("{\n" + methodBody + "}\n");
}
} catch (Exception e) {
throw new RuntimeException("problem instrumenting " + describeBefore, e);
}
}
private CtMethod makeNewMethod(CtClass ctClass, CtMethod ctMethod, CtClass returnCtClass, String methodName, CtClass[] paramTypes, String methodBody) throws CannotCompileException, NotFoundException {
return CtNewMethod.make(
ctMethod.getModifiers(),
returnCtClass,
methodName,
paramTypes,
ctMethod.getExceptionTypes(),
methodBody,
ctClass);
}
public String generateCallToSuper(String methodName, CtClass[] paramTypes) {
return "return super." + methodName + "(" + makeParameterReplacementList(paramTypes.length) + ");";
}
public String makeParameterReplacementList(int length) {
if (length == 0) {
return "";
}
String parameterReplacementList = "$1";
for (int i = 2; i <= length; ++i) {
parameterReplacementList += ", $" + i;
}
return parameterReplacementList;
}
private String generateMethodBody(CtClass ctClass, CtMethod ctMethod, boolean wasNative, boolean wasAbstract, CtClass returnCtClass, Type returnType, boolean aStatic, boolean shouldGenerateCallToSuper) throws NotFoundException {
String methodBody;
if (wasAbstract) {
methodBody = returnType.isVoid() ? "" : "return " + returnType.defaultReturnString() + ";";
} else {
methodBody = generateMethodBody(ctClass, ctMethod, returnCtClass, returnType, aStatic, shouldGenerateCallToSuper);
}
if (wasNative) {
methodBody += returnType.isVoid() ? "" : "return " + returnType.defaultReturnString() + ";";
}
return methodBody;
}
public String generateMethodBody(CtClass ctClass, CtMethod ctMethod, CtClass returnCtClass, Type returnType, boolean isStatic, boolean shouldGenerateCallToSuper) throws NotFoundException {
boolean returnsVoid = returnType.isVoid();
String className = ctClass.getName();
String methodBody;
StringBuilder buf = new StringBuilder();
buf.append("if (!");
buf.append(RobolectricInternals.class.getName());
buf.append(".shouldCallDirectly(");
buf.append(isStatic ? className + ".class" : "this");
buf.append(")) {\n");
if (!returnsVoid) {
buf.append("Object x = ");
}
buf.append(RobolectricInternals.class.getName());
buf.append(".methodInvoked(\n ");
buf.append(className);
buf.append(".class, \"");
buf.append(ctMethod.getName());
buf.append("\", ");
if (!isStatic) {
buf.append("this");
} else {
buf.append("null");
}
buf.append(", ");
appendParamTypeArray(buf, ctMethod);
buf.append(", ");
appendParamArray(buf, ctMethod);
buf.append(")");
buf.append(";\n");
if (!returnsVoid) {
buf.append("if (x != null) return ((");
buf.append(returnType.nonPrimitiveClassName(returnCtClass));
buf.append(") x)");
buf.append(returnType.unboxString());
buf.append(";\n");
if (shouldGenerateCallToSuper) {
buf.append(generateCallToSuper(ctMethod.getName(), ctMethod.getParameterTypes()));
} else {
buf.append("return ");
buf.append(returnType.defaultReturnString());
buf.append(";\n");
}
} else {
buf.append("return;\n");
}
buf.append("}\n");
methodBody = buf.toString();
return methodBody;
}
private void appendParamTypeArray(StringBuilder buf, CtMethod ctMethod) throws NotFoundException {
CtClass[] parameterTypes = ctMethod.getParameterTypes();
if (parameterTypes.length == 0) {
buf.append("new String[0]");
} else {
buf.append("new String[] {");
for (int i = 0; i < parameterTypes.length; i++) {
if (i > 0) buf.append(", ");
buf.append("\"");
CtClass parameterType = parameterTypes[i];
buf.append(parameterType.getName());
buf.append("\"");
}
buf.append("}");
}
}
private void appendParamArray(StringBuilder buf, CtMethod ctMethod) throws NotFoundException {
int parameterCount = ctMethod.getParameterTypes().length;
if (parameterCount == 0) {
buf.append("new Object[0]");
} else {
buf.append("new Object[] {");
for (int i = 0; i < parameterCount; i++) {
if (i > 0) buf.append(", ");
buf.append(RobolectricInternals.class.getName());
buf.append(".autobox(");
buf.append("$").append(i + 1);
buf.append(")");
}
buf.append("}");
}
}
private boolean declareField(CtClass ctClass, String fieldName, CtClass fieldType) throws CannotCompileException, NotFoundException {
CtMethod ctMethod = getMethod(ctClass, "get" + fieldName, "");
if (ctMethod == null) {
return false;
}
CtClass getterFieldType = ctMethod.getReturnType();
if (!getterFieldType.equals(fieldType)) {
return false;
}
if (getField(ctClass, fieldName) == null) {
CtField field = new CtField(fieldType, fieldName, ctClass);
field.setModifiers(Modifier.PRIVATE);
ctClass.addField(field);
}
return true;
}
private CtField getField(CtClass ctClass, String fieldName) {
try {
return ctClass.getField(fieldName);
} catch (NotFoundException e) {
return null;
}
}
private CtMethod getMethod(CtClass ctClass, String methodName, String desc) {
try {
return ctClass.getMethod(methodName, desc);
} catch (NotFoundException e) {
return null;
}
}
}