Home | History | Annotate | Download | only in bytecode
      1 package com.xtremelabs.robolectric.bytecode;
      2 
      3 import android.net.Uri;
      4 import com.xtremelabs.robolectric.internal.DoNotInstrument;
      5 import com.xtremelabs.robolectric.internal.Instrument;
      6 import javassist.*;
      7 
      8 import java.io.IOException;
      9 import java.util.ArrayList;
     10 import java.util.List;
     11 
     12 @SuppressWarnings({"UnusedDeclaration"})
     13 public class AndroidTranslator implements Translator {
     14     /**
     15      * IMPORTANT -- increment this number when the bytecode generated for modified classes changes
     16      * so the cache file can be invalidated.
     17      */
     18     public static final int CACHE_VERSION = 21;
     19 
     20     private static final List<ClassHandler> CLASS_HANDLERS = new ArrayList<ClassHandler>();
     21 
     22     private ClassHandler classHandler;
     23     private ClassCache classCache;
     24     private final List<String> instrumentingList = new ArrayList<String>();
     25     private final List<String> instrumentingExcludeList = new ArrayList<String>();
     26 
     27     public AndroidTranslator(ClassHandler classHandler, ClassCache classCache) {
     28         this.classHandler = classHandler;
     29         this.classCache = classCache;
     30 
     31         // Initialize lists
     32         instrumentingList.add("android.");
     33         instrumentingList.add("com.google.android.maps");
     34         instrumentingList.add("org.apache.http.impl.client.DefaultRequestDirector");
     35 
     36         instrumentingExcludeList.add("android.support.v4.app.NotificationCompat");
     37         instrumentingExcludeList.add("android.support.v4.content.LocalBroadcastManager");
     38         instrumentingExcludeList.add("android.support.v4.util.LruCache");
     39     }
     40 
     41     public AndroidTranslator(ClassHandler classHandler, ClassCache classCache, List<String> customShadowClassNames) {
     42         this(classHandler, classCache);
     43         if (customShadowClassNames != null && !customShadowClassNames.isEmpty()) {
     44             instrumentingList.addAll(customShadowClassNames);
     45         }
     46     }
     47 
     48     public void addCustomShadowClass(String customShadowClassName) {
     49         if (!instrumentingList.contains(customShadowClassName)) {
     50             instrumentingList.add(customShadowClassName);
     51         }
     52     }
     53 
     54     public static ClassHandler getClassHandler(int index) {
     55         return CLASS_HANDLERS.get(index);
     56     }
     57 
     58     @Override
     59     public void start(ClassPool classPool) throws NotFoundException, CannotCompileException {
     60         injectClassHandlerToInstrumentedClasses(classPool);
     61     }
     62 
     63     private void injectClassHandlerToInstrumentedClasses(ClassPool classPool) throws NotFoundException, CannotCompileException {
     64         int index;
     65         synchronized (CLASS_HANDLERS) {
     66             CLASS_HANDLERS.add(classHandler);
     67             index = CLASS_HANDLERS.size() - 1;
     68         }
     69 
     70         CtClass robolectricInternalsCtClass = classPool.get(RobolectricInternals.class.getName());
     71         robolectricInternalsCtClass.setModifiers(Modifier.PUBLIC);
     72 
     73         robolectricInternalsCtClass.getClassInitializer().insertBefore("{\n" +
     74                 "classHandler = " + AndroidTranslator.class.getName() + ".getClassHandler(" + index + ");\n" +
     75                 "}");
     76     }
     77 
     78     @Override
     79     public void onLoad(ClassPool classPool, String className) throws NotFoundException, CannotCompileException {
     80         if (classCache.isWriting()) {
     81             throw new IllegalStateException("shouldn't be modifying bytecode after we've started writing cache! class=" + className);
     82         }
     83 
     84         if (classHasFromAndroidEquivalent(className)) {
     85             replaceClassWithFromAndroidEquivalent(classPool, className);
     86             return;
     87         }
     88 
     89         CtClass ctClass;
     90         try {
     91             ctClass = classPool.get(className);
     92         } catch (NotFoundException e) {
     93             throw new IgnorableClassNotFoundException(e);
     94         }
     95 
     96         if (shouldInstrument(ctClass)) {
     97             int modifiers = ctClass.getModifiers();
     98             if (Modifier.isFinal(modifiers)) {
     99                 ctClass.setModifiers(modifiers & ~Modifier.FINAL);
    100             }
    101 
    102             classHandler.instrument(ctClass);
    103 
    104             fixConstructors(ctClass);
    105             fixMethods(ctClass);
    106 
    107             try {
    108                 classCache.addClass(className, ctClass.toBytecode());
    109             } catch (IOException e) {
    110                 throw new RuntimeException(e);
    111             }
    112         }
    113     }
    114 
    115     /* package */ boolean shouldInstrument(CtClass ctClass) {
    116         if (ctClass.hasAnnotation(Instrument.class)) {
    117             return true;
    118         } else if (ctClass.isInterface() || ctClass.hasAnnotation(DoNotInstrument.class)) {
    119             return false;
    120         } else {
    121             for (String klassName : instrumentingExcludeList) {
    122                 if (ctClass.getName().startsWith(klassName)) {
    123                     return false;
    124                 }
    125             }
    126             for (String klassName : instrumentingList) {
    127                 if (ctClass.getName().startsWith(klassName)) {
    128                     return true;
    129                 }
    130             }
    131             return false;
    132         }
    133     }
    134 
    135     private boolean classHasFromAndroidEquivalent(String className) {
    136         return className.startsWith(Uri.class.getName());
    137     }
    138 
    139     private void replaceClassWithFromAndroidEquivalent(ClassPool classPool, String className) throws NotFoundException {
    140         FromAndroidClassNameParts classNameParts = new FromAndroidClassNameParts(className);
    141         if (classNameParts.isFromAndroid()) return;
    142 
    143         String from = classNameParts.getNameWithFromAndroid();
    144         CtClass ctClass = classPool.getAndRename(from, className);
    145 
    146         ClassMap map = new ClassMap() {
    147             @Override
    148             public Object get(Object jvmClassName) {
    149                 FromAndroidClassNameParts classNameParts = new FromAndroidClassNameParts(jvmClassName.toString());
    150                 if (classNameParts.isFromAndroid()) {
    151                     return classNameParts.getNameWithoutFromAndroid();
    152                 } else {
    153                     return jvmClassName;
    154                 }
    155             }
    156         };
    157         ctClass.replaceClassName(map);
    158     }
    159 
    160     class FromAndroidClassNameParts {
    161         private static final String TOKEN = "__FromAndroid";
    162 
    163         private String prefix;
    164         private String suffix;
    165 
    166         FromAndroidClassNameParts(String name) {
    167             int dollarIndex = name.indexOf("$");
    168             prefix = name;
    169             suffix = "";
    170             if (dollarIndex > -1) {
    171                 prefix = name.substring(0, dollarIndex);
    172                 suffix = name.substring(dollarIndex);
    173             }
    174         }
    175 
    176         public boolean isFromAndroid() {
    177             return prefix.endsWith(TOKEN);
    178         }
    179 
    180         public String getNameWithFromAndroid() {
    181             return prefix + TOKEN + suffix;
    182         }
    183 
    184         public String getNameWithoutFromAndroid() {
    185             return prefix.replace(TOKEN, "") + suffix;
    186         }
    187     }
    188 
    189     private void addBypassShadowField(CtClass ctClass, String fieldName) {
    190         try {
    191             try {
    192                 ctClass.getField(fieldName);
    193             } catch (NotFoundException e) {
    194                 CtField field = new CtField(CtClass.booleanType, fieldName, ctClass);
    195                 field.setModifiers(java.lang.reflect.Modifier.PUBLIC | java.lang.reflect.Modifier.STATIC);
    196                 ctClass.addField(field);
    197             }
    198         } catch (CannotCompileException e) {
    199             throw new RuntimeException(e);
    200         }
    201     }
    202 
    203     private void fixConstructors(CtClass ctClass) throws CannotCompileException, NotFoundException {
    204 
    205         if (ctClass.isEnum()) {
    206             // skip enum constructors because they are not stubs in android.jar
    207             return;
    208         }
    209 
    210         boolean hasDefault = false;
    211 
    212         for (CtConstructor ctConstructor : ctClass.getDeclaredConstructors()) {
    213             try {
    214                 fixConstructor(ctClass, hasDefault, ctConstructor);
    215 
    216                 if (ctConstructor.getParameterTypes().length == 0) {
    217                     hasDefault = true;
    218                 }
    219             } catch (Exception e) {
    220                 throw new RuntimeException("problem instrumenting " + ctConstructor, e);
    221             }
    222         }
    223 
    224         if (!hasDefault) {
    225             String methodBody = generateConstructorBody(ctClass, new CtClass[0]);
    226             ctClass.addConstructor(CtNewConstructor.make(new CtClass[0], new CtClass[0], "{\n" + methodBody + "}\n", ctClass));
    227         }
    228     }
    229 
    230     private boolean fixConstructor(CtClass ctClass, boolean needsDefault, CtConstructor ctConstructor) throws NotFoundException, CannotCompileException {
    231         String methodBody = generateConstructorBody(ctClass, ctConstructor.getParameterTypes());
    232         ctConstructor.setBody("{\n" + methodBody + "}\n");
    233         return needsDefault;
    234     }
    235 
    236     private String generateConstructorBody(CtClass ctClass, CtClass[] parameterTypes) throws NotFoundException {
    237         return generateMethodBody(ctClass,
    238                 new CtMethod(CtClass.voidType, "<init>", parameterTypes, ctClass),
    239                 CtClass.voidType,
    240                 Type.VOID,
    241                 false,
    242                 false);
    243     }
    244 
    245     private void fixMethods(CtClass ctClass) throws NotFoundException, CannotCompileException {
    246         for (CtMethod ctMethod : ctClass.getDeclaredMethods()) {
    247             fixMethod(ctClass, ctMethod, true);
    248         }
    249         CtMethod equalsMethod = ctClass.getMethod("equals", "(Ljava/lang/Object;)Z");
    250         CtMethod hashCodeMethod = ctClass.getMethod("hashCode", "()I");
    251         CtMethod toStringMethod = ctClass.getMethod("toString", "()Ljava/lang/String;");
    252 
    253         fixMethod(ctClass, equalsMethod, false);
    254         fixMethod(ctClass, hashCodeMethod, false);
    255         fixMethod(ctClass, toStringMethod, false);
    256     }
    257 
    258     private String describe(CtMethod ctMethod) throws NotFoundException {
    259         return Modifier.toString(ctMethod.getModifiers()) + " " + ctMethod.getReturnType().getSimpleName() + " " + ctMethod.getLongName();
    260     }
    261 
    262     private void fixMethod(CtClass ctClass, CtMethod ctMethod, boolean wasFoundInClass) throws NotFoundException {
    263         String describeBefore = describe(ctMethod);
    264         try {
    265             CtClass declaringClass = ctMethod.getDeclaringClass();
    266             int originalModifiers = ctMethod.getModifiers();
    267 
    268             boolean wasNative = Modifier.isNative(originalModifiers);
    269             boolean wasFinal = Modifier.isFinal(originalModifiers);
    270             boolean wasAbstract = Modifier.isAbstract(originalModifiers);
    271             boolean wasDeclaredInClass = ctClass == declaringClass;
    272 
    273             if (wasFinal && ctClass.isEnum()) {
    274                 return;
    275             }
    276 
    277             int newModifiers = originalModifiers;
    278             if (wasNative) {
    279                 newModifiers = Modifier.clear(newModifiers, Modifier.NATIVE);
    280             }
    281             if (wasFinal) {
    282                 newModifiers = Modifier.clear(newModifiers, Modifier.FINAL);
    283             }
    284             if (wasFoundInClass) {
    285                 ctMethod.setModifiers(newModifiers);
    286             }
    287 
    288             CtClass returnCtClass = ctMethod.getReturnType();
    289             Type returnType = Type.find(returnCtClass);
    290 
    291             String methodName = ctMethod.getName();
    292             CtClass[] paramTypes = ctMethod.getParameterTypes();
    293 
    294 //            if (!isAbstract) {
    295 //                if (methodName.startsWith("set") && paramTypes.length == 1) {
    296 //                    String fieldName = "__" + methodName.substring(3);
    297 //                    if (declareField(ctClass, fieldName, paramTypes[0])) {
    298 //                        methodBody = fieldName + " = $1;\n" + methodBody;
    299 //                    }
    300 //                } else if (methodName.startsWith("get") && paramTypes.length == 0) {
    301 //                    String fieldName = "__" + methodName.substring(3);
    302 //                    if (declareField(ctClass, fieldName, returnType)) {
    303 //                        methodBody = "return " + fieldName + ";\n";
    304 //                    }
    305 //                }
    306 //            }
    307 
    308             boolean isStatic = Modifier.isStatic(originalModifiers);
    309             String methodBody = generateMethodBody(ctClass, ctMethod, wasNative, wasAbstract, returnCtClass, returnType, isStatic, !wasFoundInClass);
    310 
    311             if (!wasFoundInClass) {
    312                 CtMethod newMethod = makeNewMethod(ctClass, ctMethod, returnCtClass, methodName, paramTypes, "{\n" + methodBody + generateCallToSuper(methodName, paramTypes) + "\n}");
    313                 newMethod.setModifiers(newModifiers);
    314                 if (wasDeclaredInClass) {
    315                     ctMethod.insertBefore("{\n" + methodBody + "}\n");
    316                 } else {
    317                     ctClass.addMethod(newMethod);
    318                 }
    319             } else if (wasAbstract || wasNative) {
    320                 CtMethod newMethod = makeNewMethod(ctClass, ctMethod, returnCtClass, methodName, paramTypes, "{\n" + methodBody + "\n}");
    321                 ctMethod.setBody(newMethod, null);
    322             } else {
    323                 ctMethod.insertBefore("{\n" + methodBody + "}\n");
    324             }
    325         } catch (Exception e) {
    326             throw new RuntimeException("problem instrumenting " + describeBefore, e);
    327         }
    328     }
    329 
    330     private CtMethod makeNewMethod(CtClass ctClass, CtMethod ctMethod, CtClass returnCtClass, String methodName, CtClass[] paramTypes, String methodBody) throws CannotCompileException, NotFoundException {
    331         return CtNewMethod.make(
    332                 ctMethod.getModifiers(),
    333                 returnCtClass,
    334                 methodName,
    335                 paramTypes,
    336                 ctMethod.getExceptionTypes(),
    337                 methodBody,
    338                 ctClass);
    339     }
    340 
    341     public String generateCallToSuper(String methodName, CtClass[] paramTypes) {
    342         return "return super." + methodName + "(" + makeParameterReplacementList(paramTypes.length) + ");";
    343     }
    344 
    345     public String makeParameterReplacementList(int length) {
    346         if (length == 0) {
    347             return "";
    348         }
    349 
    350         String parameterReplacementList = "$1";
    351         for (int i = 2; i <= length; ++i) {
    352             parameterReplacementList += ", $" + i;
    353         }
    354         return parameterReplacementList;
    355     }
    356 
    357     private String generateMethodBody(CtClass ctClass, CtMethod ctMethod, boolean wasNative, boolean wasAbstract, CtClass returnCtClass, Type returnType, boolean aStatic, boolean shouldGenerateCallToSuper) throws NotFoundException {
    358         String methodBody;
    359         if (wasAbstract) {
    360             methodBody = returnType.isVoid() ? "" : "return " + returnType.defaultReturnString() + ";";
    361         } else {
    362             methodBody = generateMethodBody(ctClass, ctMethod, returnCtClass, returnType, aStatic, shouldGenerateCallToSuper);
    363         }
    364 
    365         if (wasNative) {
    366             methodBody += returnType.isVoid() ? "" : "return " + returnType.defaultReturnString() + ";";
    367         }
    368         return methodBody;
    369     }
    370 
    371     public String generateMethodBody(CtClass ctClass, CtMethod ctMethod, CtClass returnCtClass, Type returnType, boolean isStatic, boolean shouldGenerateCallToSuper) throws NotFoundException {
    372         boolean returnsVoid = returnType.isVoid();
    373         String className = ctClass.getName();
    374 
    375         String methodBody;
    376         StringBuilder buf = new StringBuilder();
    377         buf.append("if (!");
    378         buf.append(RobolectricInternals.class.getName());
    379         buf.append(".shouldCallDirectly(");
    380         buf.append(isStatic ? className + ".class" : "this");
    381         buf.append(")) {\n");
    382 
    383         if (!returnsVoid) {
    384             buf.append("Object x = ");
    385         }
    386         buf.append(RobolectricInternals.class.getName());
    387         buf.append(".methodInvoked(\n  ");
    388         buf.append(className);
    389         buf.append(".class, \"");
    390         buf.append(ctMethod.getName());
    391         buf.append("\", ");
    392         if (!isStatic) {
    393             buf.append("this");
    394         } else {
    395             buf.append("null");
    396         }
    397         buf.append(", ");
    398 
    399         appendParamTypeArray(buf, ctMethod);
    400         buf.append(", ");
    401         appendParamArray(buf, ctMethod);
    402 
    403         buf.append(")");
    404         buf.append(";\n");
    405 
    406         if (!returnsVoid) {
    407             buf.append("if (x != null) return ((");
    408             buf.append(returnType.nonPrimitiveClassName(returnCtClass));
    409             buf.append(") x)");
    410             buf.append(returnType.unboxString());
    411             buf.append(";\n");
    412             if (shouldGenerateCallToSuper) {
    413                 buf.append(generateCallToSuper(ctMethod.getName(), ctMethod.getParameterTypes()));
    414             } else {
    415                 buf.append("return ");
    416                 buf.append(returnType.defaultReturnString());
    417                 buf.append(";\n");
    418             }
    419         } else {
    420             buf.append("return;\n");
    421         }
    422 
    423         buf.append("}\n");
    424 
    425         methodBody = buf.toString();
    426         return methodBody;
    427     }
    428 
    429     private void appendParamTypeArray(StringBuilder buf, CtMethod ctMethod) throws NotFoundException {
    430         CtClass[] parameterTypes = ctMethod.getParameterTypes();
    431         if (parameterTypes.length == 0) {
    432             buf.append("new String[0]");
    433         } else {
    434             buf.append("new String[] {");
    435             for (int i = 0; i < parameterTypes.length; i++) {
    436                 if (i > 0) buf.append(", ");
    437                 buf.append("\"");
    438                 CtClass parameterType = parameterTypes[i];
    439                 buf.append(parameterType.getName());
    440                 buf.append("\"");
    441             }
    442             buf.append("}");
    443         }
    444     }
    445 
    446     private void appendParamArray(StringBuilder buf, CtMethod ctMethod) throws NotFoundException {
    447         int parameterCount = ctMethod.getParameterTypes().length;
    448         if (parameterCount == 0) {
    449             buf.append("new Object[0]");
    450         } else {
    451             buf.append("new Object[] {");
    452             for (int i = 0; i < parameterCount; i++) {
    453                 if (i > 0) buf.append(", ");
    454                 buf.append(RobolectricInternals.class.getName());
    455                 buf.append(".autobox(");
    456                 buf.append("$").append(i + 1);
    457                 buf.append(")");
    458             }
    459             buf.append("}");
    460         }
    461     }
    462 
    463     private boolean declareField(CtClass ctClass, String fieldName, CtClass fieldType) throws CannotCompileException, NotFoundException {
    464         CtMethod ctMethod = getMethod(ctClass, "get" + fieldName, "");
    465         if (ctMethod == null) {
    466             return false;
    467         }
    468         CtClass getterFieldType = ctMethod.getReturnType();
    469 
    470         if (!getterFieldType.equals(fieldType)) {
    471             return false;
    472         }
    473 
    474         if (getField(ctClass, fieldName) == null) {
    475             CtField field = new CtField(fieldType, fieldName, ctClass);
    476             field.setModifiers(Modifier.PRIVATE);
    477             ctClass.addField(field);
    478         }
    479 
    480         return true;
    481     }
    482 
    483     private CtField getField(CtClass ctClass, String fieldName) {
    484         try {
    485             return ctClass.getField(fieldName);
    486         } catch (NotFoundException e) {
    487             return null;
    488         }
    489     }
    490 
    491     private CtMethod getMethod(CtClass ctClass, String methodName, String desc) {
    492         try {
    493             return ctClass.getMethod(methodName, desc);
    494         } catch (NotFoundException e) {
    495             return null;
    496         }
    497     }
    498 
    499 }
    500