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