All Downloads are FREE. Search and download functionalities are using the official Maven repository.

dagger.hilt.android.plugin.AndroidEntryPointClassVisitor.kt Maven / Gradle / Ivy

There is a newer version: 2.52
Show newest version
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
  }

  abstract class Factory : AsmClassVisitorFactory {
    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?
  ) {
    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?
  ): 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"
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy