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 
AndroidTranslator(ClassHandler classHandler, ClassCache classCache)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 
AndroidTranslator(ClassHandler classHandler, ClassCache classCache, List<String> customShadowClassNames)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 
addCustomShadowClass(String customShadowClassName)48     public void addCustomShadowClass(String customShadowClassName) {
49         if (!instrumentingList.contains(customShadowClassName)) {
50             instrumentingList.add(customShadowClassName);
51         }
52     }
53 
getClassHandler(int index)54     public static ClassHandler getClassHandler(int index) {
55         return CLASS_HANDLERS.get(index);
56     }
57 
58     @Override
start(ClassPool classPool)59     public void start(ClassPool classPool) throws NotFoundException, CannotCompileException {
60         injectClassHandlerToInstrumentedClasses(classPool);
61     }
62 
injectClassHandlerToInstrumentedClasses(ClassPool classPool)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
onLoad(ClassPool classPool, String className)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 
shouldInstrument(CtClass ctClass)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 
classHasFromAndroidEquivalent(String className)135     private boolean classHasFromAndroidEquivalent(String className) {
136         return className.startsWith(Uri.class.getName());
137     }
138 
replaceClassWithFromAndroidEquivalent(ClassPool classPool, String className)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 
FromAndroidClassNameParts(String name)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 
isFromAndroid()176         public boolean isFromAndroid() {
177             return prefix.endsWith(TOKEN);
178         }
179 
getNameWithFromAndroid()180         public String getNameWithFromAndroid() {
181             return prefix + TOKEN + suffix;
182         }
183 
getNameWithoutFromAndroid()184         public String getNameWithoutFromAndroid() {
185             return prefix.replace(TOKEN, "") + suffix;
186         }
187     }
188 
addBypassShadowField(CtClass ctClass, String fieldName)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 
fixConstructors(CtClass ctClass)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 
fixConstructor(CtClass ctClass, boolean needsDefault, CtConstructor ctConstructor)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 
generateConstructorBody(CtClass ctClass, CtClass[] parameterTypes)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 
fixMethods(CtClass ctClass)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 
describe(CtMethod ctMethod)258     private String describe(CtMethod ctMethod) throws NotFoundException {
259         return Modifier.toString(ctMethod.getModifiers()) + " " + ctMethod.getReturnType().getSimpleName() + " " + ctMethod.getLongName();
260     }
261 
fixMethod(CtClass ctClass, CtMethod ctMethod, boolean wasFoundInClass)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 
makeNewMethod(CtClass ctClass, CtMethod ctMethod, CtClass returnCtClass, String methodName, CtClass[] paramTypes, String methodBody)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 
generateCallToSuper(String methodName, CtClass[] paramTypes)341     public String generateCallToSuper(String methodName, CtClass[] paramTypes) {
342         return "return super." + methodName + "(" + makeParameterReplacementList(paramTypes.length) + ");";
343     }
344 
makeParameterReplacementList(int length)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 
generateMethodBody(CtClass ctClass, CtMethod ctMethod, boolean wasNative, boolean wasAbstract, CtClass returnCtClass, Type returnType, boolean aStatic, boolean shouldGenerateCallToSuper)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 
generateMethodBody(CtClass ctClass, CtMethod ctMethod, CtClass returnCtClass, Type returnType, boolean isStatic, boolean shouldGenerateCallToSuper)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 
appendParamTypeArray(StringBuilder buf, CtMethod ctMethod)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 
appendParamArray(StringBuilder buf, CtMethod ctMethod)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 
declareField(CtClass ctClass, String fieldName, CtClass fieldType)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 
getField(CtClass ctClass, String fieldName)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 
getMethod(CtClass ctClass, String methodName, String desc)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