blob: 015bb7623f394c8d2ed78bcc52c9c566f016fe91 [file] [log] [blame]
package dagger.hilt.android.plugin
import com.android.build.api.instrumentation.AsmClassVisitorFactory
import com.android.build.api.instrumentation.ClassContext
import com.android.build.api.instrumentation.ClassData
import com.android.build.api.instrumentation.InstrumentationParameters
import java.io.File
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.FieldVisitor
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes
/**
* ASM Adapter that transforms @AndroidEntryPoint-annotated classes to extend the Hilt
* generated android class, including the @HiltAndroidApp application class.
*/
@Suppress("UnstableApiUsage")
class AndroidEntryPointClassVisitor(
private val apiVersion: Int,
nextClassVisitor: ClassVisitor,
private val additionalClasses: File
) : ClassVisitor(apiVersion, nextClassVisitor) {
interface AndroidEntryPointParams : InstrumentationParameters {
@get:Input
val additionalClassesDir: Property<File>
}
abstract class Factory : AsmClassVisitorFactory<AndroidEntryPointParams> {
override fun createClassVisitor(
classContext: ClassContext,
nextClassVisitor: ClassVisitor
): ClassVisitor {
return AndroidEntryPointClassVisitor(
apiVersion = instrumentationContext.apiVersion.get(),
nextClassVisitor = nextClassVisitor,
additionalClasses = parameters.get().additionalClassesDir.get()
)
}
/**
* Check if a class should be transformed.
*
* Only classes that are an Android entry point should be transformed.
*/
override fun isInstrumentable(classData: ClassData) =
classData.classAnnotations.any { ANDROID_ENTRY_POINT_ANNOTATIONS.contains(it) }
}
// The name of the Hilt generated superclass in it internal form.
// e.g. "my/package/Hilt_MyActivity"
lateinit var newSuperclassName: String
lateinit var oldSuperclassName: String
override fun visit(
version: Int,
access: Int,
name: String,
signature: String?,
superName: String?,
interfaces: Array<out String>?
) {
val packageName = name.substringBeforeLast('/')
val className = name.substringAfterLast('/')
newSuperclassName =
packageName + "/Hilt_" + className.replace("$", "_")
oldSuperclassName = superName ?: error { "Superclass of $name is null!" }
super.visit(version, access, name, signature, newSuperclassName, interfaces)
}
override fun visitMethod(
access: Int,
name: String,
descriptor: String,
signature: String?,
exceptions: Array<out String>?
): MethodVisitor {
val nextMethodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions)
val invokeSpecialVisitor = InvokeSpecialAdapter(apiVersion, nextMethodVisitor)
if (name == ON_RECEIVE_METHOD_NAME &&
descriptor == ON_RECEIVE_METHOD_DESCRIPTOR &&
hasOnReceiveBytecodeInjectionMarker()
) {
return OnReceiveAdapter(apiVersion, invokeSpecialVisitor)
}
return invokeSpecialVisitor
}
/**
* Adapter for super calls (e.g. super.onCreate()) that rewrites the owner reference of the
* invokespecial instruction to use the new superclass.
*
* The invokespecial instruction is emitted for code that between other things also invokes a
* method of a superclass of the current class. The opcode invokespecial takes two operands, each
* of 8 bit, that together represent an address in the constant pool to a method reference. The
* method reference is computed at compile-time by looking the direct superclass declaration, but
* at runtime the code behaves like invokevirtual, where as the actual method invoked is looked up
* based on the class hierarchy.
*
* However, it has been observed that on APIs 19 to 22 the Android Runtime (ART) jumps over the
* direct superclass and into the method reference class, causing unexpected behaviours.
* Therefore, this method performs the additional transformation to rewrite direct super call
* invocations to use a method reference whose class in the pool is the new superclass. Note that
* this is not necessary for constructor calls since the Javassist library takes care of those.
*
* @see: https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-6.html#jvms-6.5.invokespecial
* @see: https://source.android.com/devices/tech/dalvik/dalvik-bytecode
*/
inner class InvokeSpecialAdapter(
apiVersion: Int,
nextClassVisitor: MethodVisitor
) : MethodVisitor(apiVersion, nextClassVisitor) {
override fun visitMethodInsn(
opcode: Int,
owner: String,
name: String,
descriptor: String,
isInterface: Boolean
) {
if (opcode == Opcodes.INVOKESPECIAL && owner == oldSuperclassName) {
// Update the owner of all INVOKESPECIAL instructions, including those found in
// constructors.
super.visitMethodInsn(opcode, newSuperclassName, name, descriptor, isInterface)
} else {
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface)
}
}
}
/**
* Method adapter for a BroadcastReceiver's onReceive method to insert a super call since with
* its new superclass, onReceive will no longer be abstract (it is implemented by Hilt generated
* receiver).
*/
inner class OnReceiveAdapter(
apiVersion: Int,
nextClassVisitor: MethodVisitor
) : MethodVisitor(apiVersion, nextClassVisitor) {
override fun visitCode() {
super.visitCode()
super.visitIntInsn(Opcodes.ALOAD, 0) // Load 'this'
super.visitIntInsn(Opcodes.ALOAD, 1) // Load method param 1 (Context)
super.visitIntInsn(Opcodes.ALOAD, 2) // Load method param 2 (Intent)
super.visitMethodInsn(
Opcodes.INVOKESPECIAL,
newSuperclassName,
ON_RECEIVE_METHOD_NAME,
ON_RECEIVE_METHOD_DESCRIPTOR,
false
)
}
}
/**
* Check if Hilt generated class is a BroadcastReceiver with the marker field which means
* a super.onReceive invocation has to be inserted in the implementation.
*/
private fun hasOnReceiveBytecodeInjectionMarker() =
findAdditionalClassFile(newSuperclassName).inputStream().use {
var hasMarker = false
ClassReader(it).accept(
object : ClassVisitor(apiVersion) {
override fun visitField(
access: Int,
name: String,
descriptor: String,
signature: String?,
value: Any?
): FieldVisitor? {
if (name == "onReceiveBytecodeInjectionMarker") {
hasMarker = true
}
return null
}
},
ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES
)
return@use hasMarker
}
private fun findAdditionalClassFile(className: String) =
File(additionalClasses, "$className.class")
companion object {
val ANDROID_ENTRY_POINT_ANNOTATIONS = setOf(
"dagger.hilt.android.AndroidEntryPoint",
"dagger.hilt.android.HiltAndroidApp"
)
const val ON_RECEIVE_METHOD_NAME = "onReceive"
const val ON_RECEIVE_METHOD_DESCRIPTOR =
"(Landroid/content/Context;Landroid/content/Intent;)V"
}
}