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

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

There is a newer version: 2.52
Show newest version
/*
 * Copyright (C) 2020 The Dagger Authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package dagger.hilt.android.plugin

import com.android.build.api.transform.DirectoryInput
import com.android.build.api.transform.Format
import com.android.build.api.transform.JarInput
import com.android.build.api.transform.QualifiedContent
import com.android.build.api.transform.Status
import com.android.build.api.transform.Transform
import com.android.build.api.transform.TransformInput
import com.android.build.api.transform.TransformInvocation
import dagger.hilt.android.plugin.util.isClassFile
import java.io.File

/**
 * Bytecode transformation to make @AndroidEntryPoint annotated classes extend the Hilt
 * generated android classes, including the @HiltAndroidApp application class.
 *
 * A transform receives input as a collection [TransformInput], which is composed of [JarInput]s and
 * [DirectoryInput]s. The resulting files must be placed in the
 * [TransformInvocation.getOutputProvider]. The bytecode transformation can be done with any library
 * (in our case Javaassit). The [QualifiedContent.Scope] defined in a transform defines the input
 * the transform will receive and if it can be applied to only the Android application projects or
 * Android libraries too.
 *
 * See: [TransformPublic Docs](https://google.github.io/android-gradle-dsl/javadoc/current/com/android/build/api/transform/Transform.html)
 */
class AndroidEntryPointTransform : Transform() {
  // The name of the transform. This name appears as a gradle task.
  override fun getName() = "AndroidEntryPointTransform"

  // The type of input this transform will handle.
  override fun getInputTypes() = setOf(QualifiedContent.DefaultContentType.CLASSES)

  override fun isIncremental() = true

  // The project scope this transform is applied to.
  override fun getScopes() = mutableSetOf(QualifiedContent.Scope.PROJECT)

  /**
   * Performs the transformation of the bytecode.
   *
   * The inputs will be available in the [TransformInvocation] along with referenced inputs that
   * should not be transformed. The inputs received along with the referenced inputs depend on the
   * scope of the transform.
   *
   * The invocation will also indicate if an incremental transform has to be applied or not. Even
   * though a transform might return true in its [isIncremental] function, the invocation might
   * return false in [TransformInvocation.isIncremental], therefore both cases must be handled.
   */
  override fun transform(invocation: TransformInvocation) {
    if (!invocation.isIncremental) {
      // Remove any lingering files on a non-incremental invocation since everything has to be
      // transformed.
      invocation.outputProvider.deleteAll()
    }

    invocation.inputs.forEach { transformInput ->
      transformInput.jarInputs.forEach { jarInput ->
        val outputJar =
          invocation.outputProvider.getContentLocation(
            jarInput.name,
            jarInput.contentTypes,
            jarInput.scopes,
            Format.JAR
          )
        if (invocation.isIncremental) {
          when (jarInput.status) {
            Status.ADDED, Status.CHANGED -> copyJar(jarInput.file, outputJar)
            Status.REMOVED -> outputJar.delete()
            Status.NOTCHANGED -> {
              // No need to transform.
            }
            else -> {
              error("Unknown status: ${jarInput.status}")
            }
          }
        } else {
          copyJar(jarInput.file, outputJar)
        }
      }
      transformInput.directoryInputs.forEach { directoryInput ->
        val outputDir = invocation.outputProvider.getContentLocation(
          directoryInput.name,
          directoryInput.contentTypes,
          directoryInput.scopes,
          Format.DIRECTORY
        )
        val classTransformer =
          createHiltClassTransformer(invocation.inputs, invocation.referencedInputs, outputDir)
        if (invocation.isIncremental) {
          directoryInput.changedFiles.forEach { (file, status) ->
            val outputFile = toOutputFile(outputDir, directoryInput.file, file)
            when (status) {
              Status.ADDED, Status.CHANGED ->
                transformFile(file, outputFile.parentFile, classTransformer)
              Status.REMOVED -> outputFile.delete()
              Status.NOTCHANGED -> {
                // No need to transform.
              }
              else -> {
                error("Unknown status: $status")
              }
            }
          }
        } else {
          directoryInput.file.walkTopDown().forEach { file ->
            val outputFile = toOutputFile(outputDir, directoryInput.file, file)
            transformFile(file, outputFile.parentFile, classTransformer)
          }
        }
      }
    }
  }

  // Create a transformer given an invocation inputs. Note that since this is a PROJECT scoped
  // transform the actual transformation is only done on project files and not its dependencies.
  private fun createHiltClassTransformer(
    inputs: Collection,
    referencedInputs: Collection,
    outputDir: File
  ): AndroidEntryPointClassTransformer {
    val classFiles = (inputs + referencedInputs).flatMap { input ->
      (input.directoryInputs + input.jarInputs).map { it.file }
    }
    return AndroidEntryPointClassTransformer(
      taskName = name,
      allInputs = classFiles,
      sourceRootOutputDir = outputDir,
      copyNonTransformed = true
    )
  }

  // Transform a single file. If the file is not a class file it is just copied to the output dir.
  private fun transformFile(
    inputFile: File,
    outputDir: File,
    transformer: AndroidEntryPointClassTransformer
  ) {
    if (inputFile.isClassFile()) {
      transformer.transformFile(inputFile)
    } else if (inputFile.isFile) {
      // Copy all non .class files to the output.
      outputDir.mkdirs()
      val outputFile = File(outputDir, inputFile.name)
      inputFile.copyTo(target = outputFile, overwrite = true)
    }
  }

  // We are only interested in project compiled classes but we have to copy received jars to the
  // output.
  private fun copyJar(inputJar: File, outputJar: File) {
    outputJar.parentFile?.mkdirs()
    inputJar.copyTo(target = outputJar, overwrite = true)
  }

  private fun toOutputFile(outputDir: File, inputDir: File, inputFile: File) =
    File(outputDir, inputFile.relativeTo(inputDir).path)
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy