• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.Internal
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 class AndroidEntryPointClassVisitor(
21   private val apiVersion: Int,
22   nextClassVisitor: ClassVisitor,
23   private val additionalClasses: File
24 ) : ClassVisitor(apiVersion, nextClassVisitor) {
25 
26   @Suppress("UnstableApiUsage") // ASM Pipeline APIs
27   interface AndroidEntryPointParams : InstrumentationParameters {
28     @get:Internal
29     val additionalClassesDir: Property<File>
30   }
31 
32   @Suppress("UnstableApiUsage") // ASM Pipeline APIs
33   abstract class Factory : AsmClassVisitorFactory<AndroidEntryPointParams> {
createClassVisitornull34     override fun createClassVisitor(
35       classContext: ClassContext,
36       nextClassVisitor: ClassVisitor
37     ): ClassVisitor {
38       return AndroidEntryPointClassVisitor(
39         apiVersion = instrumentationContext.apiVersion.get(),
40         nextClassVisitor = nextClassVisitor,
41         additionalClasses = parameters.get().additionalClassesDir.get()
42       )
43     }
44 
45     /**
46      * Check if a class should be transformed.
47      *
48      * Only classes that are an Android entry point should be transformed.
49      */
isInstrumentablenull50     override fun isInstrumentable(classData: ClassData) =
51       classData.classAnnotations.any { ANDROID_ENTRY_POINT_ANNOTATIONS.contains(it) }
52   }
53 
54   // The name of the Hilt generated superclass in it internal form.
55   // e.g. "my/package/Hilt_MyActivity"
56   lateinit var newSuperclassName: String
57 
58   lateinit var oldSuperclassName: String
59 
visitnull60   override fun visit(
61     version: Int,
62     access: Int,
63     name: String,
64     signature: String?,
65     superName: String?,
66     interfaces: Array<out String>?
67   ) {
68     val packageName = name.substringBeforeLast('/')
69     val className = name.substringAfterLast('/')
70     newSuperclassName =
71       packageName + "/Hilt_" + className.replace("$", "_")
72     oldSuperclassName = superName ?: error { "Superclass of $name is null!" }
73     val newSignature = signature?.replaceFirst(oldSuperclassName, newSuperclassName)
74     super.visit(version, access, name, newSignature, newSuperclassName, interfaces)
75   }
76 
visitMethodnull77   override fun visitMethod(
78     access: Int,
79     name: String,
80     descriptor: String,
81     signature: String?,
82     exceptions: Array<out String>?
83   ): MethodVisitor {
84     val nextMethodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions)
85     val invokeSpecialVisitor = InvokeSpecialAdapter(
86       apiVersion = apiVersion,
87       nextClassVisitor = nextMethodVisitor,
88       isConstructor = name == "<init>"
89     )
90     if (name == ON_RECEIVE_METHOD_NAME &&
91       descriptor == ON_RECEIVE_METHOD_DESCRIPTOR &&
92       hasOnReceiveBytecodeInjectionMarker()
93     ) {
94       return OnReceiveAdapter(apiVersion, invokeSpecialVisitor)
95     }
96     return invokeSpecialVisitor
97   }
98 
99   /**
100    * Adapter for super calls (e.g. super.onCreate()) that rewrites the owner reference of the
101    * invokespecial instruction to use the new superclass.
102    *
103    * The invokespecial instruction is emitted for code that between other things also invokes a
104    * method of a superclass of the current class. The opcode invokespecial takes two operands, each
105    * of 8 bit, that together represent an address in the constant pool to a method reference. The
106    * method reference is computed at compile-time by looking the direct superclass declaration, but
107    * at runtime the code behaves like invokevirtual, where as the actual method invoked is looked up
108    * based on the class hierarchy.
109    *
110    * However, it has been observed that on APIs 19 to 22 the Android Runtime (ART) jumps over the
111    * direct superclass and into the method reference class, causing unexpected behaviours.
112    * Therefore, this method performs the additional transformation to rewrite direct super call
113    * invocations to use a method reference whose class in the pool is the new superclass.
114    *
115    * @see: https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-6.html#jvms-6.5.invokespecial
116    * @see: https://source.android.com/devices/tech/dalvik/dalvik-bytecode
117    */
118   inner class InvokeSpecialAdapter(
119     apiVersion: Int,
120     nextClassVisitor: MethodVisitor,
121     private val isConstructor: Boolean
122   ) : MethodVisitor(apiVersion, nextClassVisitor) {
123 
124     // Flag to know that we have visited the first invokespecial instruction in a constructor call
125     // which corresponds to the `super()` constructor call required as the first statement of an
126     // overridden constructor body.
127     private var visitedSuperConstructorInvokeSpecial = false
128 
visitMethodInsnnull129     override fun visitMethodInsn(
130       opcode: Int,
131       owner: String,
132       name: String,
133       descriptor: String,
134       isInterface: Boolean
135     ) {
136       if (opcode == Opcodes.INVOKESPECIAL && owner == oldSuperclassName) {
137         // Update the owner of INVOKESPECIAL instructions, including those found in constructors.
138         super.visitMethodInsn(opcode, getAdaptedOwner(name) ?: owner, name, descriptor, isInterface)
139       } else {
140         super.visitMethodInsn(opcode, owner, name, descriptor, isInterface)
141       }
142     }
143 
144     // Gets the updated owner of an INVOKESPECIAL found in the method being visited.
getAdaptedOwnernull145     private fun getAdaptedOwner(methodRefName: String): String? {
146       // If the method reference is a constructor and we are visiting a constructor then only the
147       // first INVOKESPECIAL instruction found should be transformed since that correponds to the
148       // super constructor call.
149       if (methodRefName == "<init>" && isConstructor && !visitedSuperConstructorInvokeSpecial) {
150         visitedSuperConstructorInvokeSpecial = true
151         return newSuperclassName
152       }
153       // If the method reference is not a constructor then the instruction for a super call that
154       // should be transformed.
155       if (methodRefName != "<init>") {
156         return newSuperclassName
157       }
158       return null
159     }
160   }
161 
162   /**
163    * Method adapter for a BroadcastReceiver's onReceive method to insert a super call since with
164    * its new superclass, onReceive will no longer be abstract (it is implemented by Hilt generated
165    * receiver).
166    */
167   inner class OnReceiveAdapter(
168     apiVersion: Int,
169     nextClassVisitor: MethodVisitor
170   ) : MethodVisitor(apiVersion, nextClassVisitor) {
visitCodenull171     override fun visitCode() {
172       super.visitCode()
173       super.visitIntInsn(Opcodes.ALOAD, 0) // Load 'this'
174       super.visitIntInsn(Opcodes.ALOAD, 1) // Load method param 1 (Context)
175       super.visitIntInsn(Opcodes.ALOAD, 2) // Load method param 2 (Intent)
176       super.visitMethodInsn(
177         Opcodes.INVOKESPECIAL,
178         newSuperclassName,
179         ON_RECEIVE_METHOD_NAME,
180         ON_RECEIVE_METHOD_DESCRIPTOR,
181         false
182       )
183     }
184   }
185 
186   /**
187    * Check if Hilt generated class is a BroadcastReceiver with the marker field which means
188    * a super.onReceive invocation has to be inserted in the implementation.
189    */
hasOnReceiveBytecodeInjectionMarkernull190   private fun hasOnReceiveBytecodeInjectionMarker() =
191     findAdditionalClassFile(newSuperclassName).inputStream().use {
192       var hasMarker = false
193       ClassReader(it).accept(
194         object : ClassVisitor(apiVersion) {
195           override fun visitField(
196             access: Int,
197             name: String,
198             descriptor: String,
199             signature: String?,
200             value: Any?
201           ): FieldVisitor? {
202             if (name == "onReceiveBytecodeInjectionMarker") {
203               hasMarker = true
204             }
205             return null
206           }
207         },
208         ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES
209       )
210       return@use hasMarker
211     }
212 
findAdditionalClassFilenull213   private fun findAdditionalClassFile(className: String) =
214     File(additionalClasses, "$className.class")
215 
216   companion object {
217     val ANDROID_ENTRY_POINT_ANNOTATIONS = setOf(
218       "dagger.hilt.android.AndroidEntryPoint",
219       "dagger.hilt.android.HiltAndroidApp"
220     )
221     const val ON_RECEIVE_METHOD_NAME = "onReceive"
222     const val ON_RECEIVE_METHOD_DESCRIPTOR =
223       "(Landroid/content/Context;Landroid/content/Intent;)V"
224   }
225 }
226