1 package dagger.hilt.android.plugin
2 
3 import com.android.build.api.instrumentation.AsmClassVisitorFactory
4 import com.android.build.api.instrumentation.ClassContext
5 import com.android.build.api.instrumentation.ClassData
6 import com.android.build.api.instrumentation.InstrumentationParameters
7 import java.io.File
8 import org.gradle.api.provider.Property
9 import org.gradle.api.tasks.Input
10 import org.objectweb.asm.ClassReader
11 import org.objectweb.asm.ClassVisitor
12 import org.objectweb.asm.FieldVisitor
13 import org.objectweb.asm.MethodVisitor
14 import org.objectweb.asm.Opcodes
15 
16 /**
17  * ASM Adapter that transforms @AndroidEntryPoint-annotated classes to extend the Hilt
18  * generated android class, including the @HiltAndroidApp application class.
19  */
20 @Suppress("UnstableApiUsage")
21 class AndroidEntryPointClassVisitor(
22   private val apiVersion: Int,
23   nextClassVisitor: ClassVisitor,
24   private val additionalClasses: File
25 ) : ClassVisitor(apiVersion, nextClassVisitor) {
26 
27   interface AndroidEntryPointParams : InstrumentationParameters {
28     @get:Input
29     val additionalClassesDir: Property<File>
30   }
31 
32   abstract class Factory : AsmClassVisitorFactory<AndroidEntryPointParams> {
createClassVisitornull33     override fun createClassVisitor(
34       classContext: ClassContext,
35       nextClassVisitor: ClassVisitor
36     ): ClassVisitor {
37       return AndroidEntryPointClassVisitor(
38         apiVersion = instrumentationContext.apiVersion.get(),
39         nextClassVisitor = nextClassVisitor,
40         additionalClasses = parameters.get().additionalClassesDir.get()
41       )
42     }
43 
44     /**
45      * Check if a class should be transformed.
46      *
47      * Only classes that are an Android entry point should be transformed.
48      */
isInstrumentablenull49     override fun isInstrumentable(classData: ClassData) =
50       classData.classAnnotations.any { ANDROID_ENTRY_POINT_ANNOTATIONS.contains(it) }
51   }
52 
53   // The name of the Hilt generated superclass in it internal form.
54   // e.g. "my/package/Hilt_MyActivity"
55   lateinit var newSuperclassName: String
56 
57   lateinit var oldSuperclassName: String
58 
visitnull59   override fun visit(
60     version: Int,
61     access: Int,
62     name: String,
63     signature: String?,
64     superName: String?,
65     interfaces: Array<out String>?
66   ) {
67     val packageName = name.substringBeforeLast('/')
68     val className = name.substringAfterLast('/')
69     newSuperclassName =
70       packageName + "/Hilt_" + className.replace("$", "_")
71     oldSuperclassName = superName ?: error { "Superclass of $name is null!" }
72     super.visit(version, access, name, signature, newSuperclassName, interfaces)
73   }
74 
visitMethodnull75   override fun visitMethod(
76     access: Int,
77     name: String,
78     descriptor: String,
79     signature: String?,
80     exceptions: Array<out String>?
81   ): MethodVisitor {
82     val nextMethodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions)
83     val invokeSpecialVisitor = InvokeSpecialAdapter(apiVersion, nextMethodVisitor)
84     if (name == ON_RECEIVE_METHOD_NAME &&
85       descriptor == ON_RECEIVE_METHOD_DESCRIPTOR &&
86       hasOnReceiveBytecodeInjectionMarker()
87     ) {
88       return OnReceiveAdapter(apiVersion, invokeSpecialVisitor)
89     }
90     return invokeSpecialVisitor
91   }
92 
93   /**
94    * Adapter for super calls (e.g. super.onCreate()) that rewrites the owner reference of the
95    * invokespecial instruction to use the new superclass.
96    *
97    * The invokespecial instruction is emitted for code that between other things also invokes a
98    * method of a superclass of the current class. The opcode invokespecial takes two operands, each
99    * of 8 bit, that together represent an address in the constant pool to a method reference. The
100    * method reference is computed at compile-time by looking the direct superclass declaration, but
101    * at runtime the code behaves like invokevirtual, where as the actual method invoked is looked up
102    * based on the class hierarchy.
103    *
104    * However, it has been observed that on APIs 19 to 22 the Android Runtime (ART) jumps over the
105    * direct superclass and into the method reference class, causing unexpected behaviours.
106    * Therefore, this method performs the additional transformation to rewrite direct super call
107    * invocations to use a method reference whose class in the pool is the new superclass. Note that
108    * this is not necessary for constructor calls since the Javassist library takes care of those.
109    *
110    * @see: https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-6.html#jvms-6.5.invokespecial
111    * @see: https://source.android.com/devices/tech/dalvik/dalvik-bytecode
112    */
113   inner class InvokeSpecialAdapter(
114     apiVersion: Int,
115     nextClassVisitor: MethodVisitor
116   ) : MethodVisitor(apiVersion, nextClassVisitor) {
visitMethodInsnnull117     override fun visitMethodInsn(
118       opcode: Int,
119       owner: String,
120       name: String,
121       descriptor: String,
122       isInterface: Boolean
123     ) {
124       if (opcode == Opcodes.INVOKESPECIAL && owner == oldSuperclassName) {
125         // Update the owner of all INVOKESPECIAL instructions, including those found in
126         // constructors.
127         super.visitMethodInsn(opcode, newSuperclassName, name, descriptor, isInterface)
128       } else {
129         super.visitMethodInsn(opcode, owner, name, descriptor, isInterface)
130       }
131     }
132   }
133 
134   /**
135    * Method adapter for a BroadcastReceiver's onReceive method to insert a super call since with
136    * its new superclass, onReceive will no longer be abstract (it is implemented by Hilt generated
137    * receiver).
138    */
139   inner class OnReceiveAdapter(
140     apiVersion: Int,
141     nextClassVisitor: MethodVisitor
142   ) : MethodVisitor(apiVersion, nextClassVisitor) {
visitCodenull143     override fun visitCode() {
144       super.visitCode()
145       super.visitIntInsn(Opcodes.ALOAD, 0) // Load 'this'
146       super.visitIntInsn(Opcodes.ALOAD, 1) // Load method param 1 (Context)
147       super.visitIntInsn(Opcodes.ALOAD, 2) // Load method param 2 (Intent)
148       super.visitMethodInsn(
149         Opcodes.INVOKESPECIAL,
150         newSuperclassName,
151         ON_RECEIVE_METHOD_NAME,
152         ON_RECEIVE_METHOD_DESCRIPTOR,
153         false
154       )
155     }
156   }
157 
158   /**
159    * Check if Hilt generated class is a BroadcastReceiver with the marker field which means
160    * a super.onReceive invocation has to be inserted in the implementation.
161    */
hasOnReceiveBytecodeInjectionMarkernull162   private fun hasOnReceiveBytecodeInjectionMarker() =
163     findAdditionalClassFile(newSuperclassName).inputStream().use {
164       var hasMarker = false
165       ClassReader(it).accept(
166         object : ClassVisitor(apiVersion) {
167           override fun visitField(
168             access: Int,
169             name: String,
170             descriptor: String,
171             signature: String?,
172             value: Any?
173           ): FieldVisitor? {
174             if (name == "onReceiveBytecodeInjectionMarker") {
175               hasMarker = true
176             }
177             return null
178           }
179         },
180         ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES
181       )
182       return@use hasMarker
183     }
184 
findAdditionalClassFilenull185   private fun findAdditionalClassFile(className: String) =
186     File(additionalClasses, "$className.class")
187 
188   companion object {
189     val ANDROID_ENTRY_POINT_ANNOTATIONS = setOf(
190       "dagger.hilt.android.AndroidEntryPoint",
191       "dagger.hilt.android.HiltAndroidApp"
192     )
193     const val ON_RECEIVE_METHOD_NAME = "onReceive"
194     const val ON_RECEIVE_METHOD_DESCRIPTOR =
195       "(Landroid/content/Context;Landroid/content/Intent;)V"
196   }
197 }
198