Skip to content
Book Cover of Extending Android Builds

3-6: Polyfill Toolkit for Artifact Enhancement ​

Given the rapid introduction of new Artifacts, there are short-term requirements that remain unaddressed. Hence, it's evident that the community needs to rally around and offer solutions. Herein, we'll lean into the foundational principles of the Artifact API, melding them with the best practices from "Task writing for AGP Synergy Plugins" (Section 3-2), to fashion a series of supplemental Artifact sets dubbed the "Polyfill" toolkit.

In software parlance, particularly in web development, "Polyfill" signifies a "patch" that bridges the divide between developer intent and the user runtime environment. For instance, it could enable IE7 to mimic the HTML Canvas element using the Silverlight plugin, or emulate CSS's support for the rem unit, among other functionalities. Jetpack Compose offers a similar informal component library titled "Accompanist". While its primary objective is to beta-test novel components, it doesn’t pledge binary compatibility or consistent performance across upgrades. However, its true worth shines as it addresses user challenges, serving as a community-driven open-source solution, co-developed with the Compose team.

The Polyfill toolkit for AGP draws inspiration from this ethos. It strives to bridge the void between third-party Plugins and AGP's official Artifact API. Through this, we've enriched the existing Artifact functionalities, allowing operations like accessing resource folders before and after merging, previewing all AndroidManifest.xml instances pre-merge, tapping into utility Artifacts under InternalArtifactType, and initiating more bespoke Artifacts.

Even though Polyfill is mentioned in many of the sample projects of this book, it's important to note that it's more of a feasible design than an official standard.

For the ensuing code references, we are aligning with Polyfill v0.8.1. The repository can be accessed by this link: https://github.com/2BAB/Polyfill/tree/0.8.1.

3-6-1: Outlining the Polyfill Design ​

While the preceding sections discussed Artifact API traceability, laying the groundwork for Polyfill's traceable architecture, we need to circle back to the core intent and purpose of Polyfill:

Our aspiration is for AGP to unveil more Artifact touchpoints, promoting symbiotic engagements with third-party Plugins. Our guiding principle is to lay out tangible challenges and real-world use cases for the Android Gradle Plugin team. This includes advancing open Artifact proposals via Google's issue tracker. Simultaneously, we acknowledge that the current offerings might not suffice for common developmental requirements, hence the pivot towards a third-party Artifact API.

AGP's cardinal mission has always been to ensure the completeness and consistency of the Android Application build process. Drawing parallels, the connection between Polyfill and AGP mirrors that between Accompanist and Jetpack Compose. Polyfill augments AGP's capabilities, not by generating Artifacts but by enhancing AGP's ability to organize and export more. In an ideal future, Polyfill might become redundant, as all Plugins integrate seamlessly with AGP's public API, catering to the majority of developer requirements.

With this vision in tow, three pivotal design facets for Polyfill crystallize:

  1. Ensuring seamless alignment with the style and lifecycle of AGP’s Artifact API, facilitating eventual migration to the official API.
  2. Empowering developers to embed customized logic without intensive overheads. What framework allows for smooth scalability?
  3. Strategizing for the long haul: How can we stay abreast of AGP's evolutions, keep Polyfill's API updated, and ensure compatibility across AGP versions?

Let’s delve deeper into each facet.

3-6-2: Aligning Polyfill's API with AGP's Standards ​

Our primary focus is the interaction paradigm related to Artifact access APIs, instances being variant.artifacts, trailed by operations like get(...)/getAll(...)/use(...). Conceptually, this implies appending an extra property to the Variant oriented objects using Kotlin’s extension property (though it's not elegant in Java or Groovy callings):

Kotlin
val ApplicationVariant.artifactsPolyfill: ApplicationArtifactsRepository
  get() = ...

A deep dive into Sections 3-4 reveals that the foundational Artifacts are scaffolded atop a basic cache system. This can be architected, taking cues from AGP's corresponding API. The appended attributes invariably return an ApplicationArtifactsRepository instance. Let's shift our lens to its interface next.

Kotlin
// Similar to Artifact APIs
interface ArtifactsRepository<PluginTypeT : PolyfilledPluginType> {

  fun <FileTypeT : FileSystemLocation> get(
    type: PolyfilledSingleArtifact<FileTypeT, PluginTypeT>
  ): Provider<FileTypeT>

  fun <FILE_TYPE : FileSystemLocation> get(
    type: Artifact.Single<FILE_TYPE>
  ): Provider<FILE_TYPE>

  fun <FileTypeT : FileSystemLocation> getAll(
    type: PolyfilledMultipleArtifact<FileTypeT, PluginTypeT>
  ): Provider<List<FileTypeT>>
  
  fun <FILE_TYPE : FileSystemLocation> getAll(
    type: Artifact.Multiple<FILE_TYPE>
  ): Provider<List<FILE_TYPE>>

  fun <FileTypeT : FileSystemLocation> use(
    action: PolyfillAction<FileTypeT>,
    toInPlaceUpdate: PolyfilledSingleArtifact<FileTypeT, PluginTypeT>
  )
   
  fun <FileTypeT : FileSystemLocation> use(
    action: PolyfillAction<List<FileTypeT>>,
    toInPlaceUpdate: PolyfilledMultipleArtifact<FileTypeT, PluginTypeT>
  )

}

...

// "Single" Artifacts
sealed class PolyfilledSingleArtifact<FileTypeT : FileSystemLocation,
    PluginTypeT : PolyfilledPluginType>(kind: ArtifactKind<FileTypeT>) :
  PolyfilledArtifact<FileTypeT>(kind) {

  object MERGED_RESOURCES :
    PolyfilledSingleArtifact<Directory, PolyfilledApplicationArtifact>(
    ArtifactKind.DIRECTORY)
}

// "Multiple" Artifacts
sealed class PolyfilledMultipleArtifact<FileTypeT : FileSystemLocation,
    PluginTypeT : PolyfilledPluginType>(kind: ArtifactKind<FileTypeT>) :
  PolyfilledArtifact<FileTypeT>(kind) {

  object ALL_MANIFESTS :
    PolyfilledMultipleArtifact<RegularFile, PolyfilledApplicationArtifact>(
    ArtifactKind.FILE)

  ...
}

Here we have referenced the plain design of Artifacts, simplified the part of StorageProviderImpl, and directly exposed ArtifactContainer, i.e., ArtifactsRepository.

3-6-3: Tying into AGP's Lifecycle ​

Before diving deeper into caching intricacies, let's reconsider artifactsPolyfill's get() method. We should strategize on when and how the ArtifactsRepository comes into being. Recollecting our brief mention of Section 3-4, we can utilize the AndroidComponentsExtension#registerExtension(...), an incubating public API yet to be documented.

Kotlin
interface AndroidComponentsExtension<
    DslExtensionT: CommonExtension<*, *, *, *>,
    VariantBuilderT: VariantBuilder,
    VariantT: Variant>
  : DslLifecycle<DslExtensionT> {
  
  @Incubating
  fun registerExtension(
    dslExtension: DslExtension,
    configurator: (v: VariantExtensionConfig<VariantT>) -> VariantExtension
  )
  
}

This API lets us create a DslExtension and attach it to the AGP Variant, ensuring it shares the Variant's lifecycle. And though we're not leveraging its DSL-related capabilities, we're utilizing them to craft a pure utility class extension.

Kotlin
class PolyfillPlugin : Plugin<Project> {

   override fun apply(project: Project) {
    ...

    project.plugins.withType<AppPlugin> {
      val androidExt = project.extensions.getByType(
        ApplicationAndroidComponentsExtension::class.java
      )

      val proxyDslExt = DslExtension.Builder(
        ApplicationArtifactsRepository::class.simpleName!!).build()
      androidExt.registerExtension(proxyDslExt) { variantExtConfig ->
        val artifactsPolyfill = ApplicationArtifactsRepository(
          project, variantExtConfig.variant)
        artifactsPolyfills.add(artifactsPolyfill)
        artifactsPolyfill
      }

    }
  }
  
}

Once initialized, you can harness Variant#getExtension(...) to retrieve the identical extension instance.

Kotlin
val ApplicationVariant.artifactsPolyfill: ApplicationArtifactsRepository
  get() = getExtension(ApplicationArtifactsRepository::class.java)
    ?: throw PolyfillUninitializedException()

Reflecting upon Section 3-3, Variant Artifacts operations typically transpire within the onVariant(...) callback. Meanwhile, Section 3-4 reveals that the configurator from registerExtension(...) is invoked post-Variant creation, preceding the onVariants(...) callback. This seamless setup ensures our ApplicationArtifactsRepository is instantiated prior to other developers operations, eliminating lifecycle concerns.

3-6-4: Implementation: Tasks and PolyfillAction ​

Moving forward, let's talk about the mechanism for interfacing with the Polyfill Artifact, particularly through "fetching" (get(...)/getAll(...)) and "modifying" (use()).

Traditionally, with the AGP Artifacts API, developers employ a custom Gradle Task to handle incoming Artifact-related parameters. Our approach with Polyfill aligns with this, suggesting custom Tasks for acquisitions. These acquisitions frequently bear dependency details, facilitating the execution of commands like assembleTask.dependsOn(customTask).

For Artifact modifications, our choice, as detailed in Section 3-2, is the Task Action method. As Gradle doesn't present a Task Action interface definition (it's merely an annotation), we've custom-crafted the PolyfillAction interface for the sake of convenience. This lets users come up with a way for Artifact processing to be sent into Polyfill.

Kotlin
interface PolyfillAction<ArtifactType> {
  fun onTaskConfigure(task: Task)
  fun onExecute(artifact: Provider<ArtifactType>)
}

Here:

  • onTaskConfigure(task: Task) enables Task advanced configuration. Developers can integrate extra Task Inputs based on the Gradle Task Runtime API here.
  • onExecute(artifact: Provider<ArtifactType>) enables Artifact alteration, letting developers slot in specific Artifact modification logic. It's worth noting that we primarily advocate for in-place modifications rather than transforming these Artifacts.

An example:

Kotlin
class PreUpdateManifestsTaskAction: PolyfillAction<List<RegularFile>> {

  override fun onTaskConfigure(task: Task) {
    task.inputs.files("...")
    task.inputs.property("key", "value")
    ...  
  }

  override fun onExecute(artifact: Provider<List<RegularFile>>) {
    artifact.get().let { files ->
      files.forEach {
        val manifestFile = it.asFile
        // Iterate through AndroidManifest.xml input, 
        // potential fixes or changes can be done here.
      }
    }
  }
}

Circling back to the Polyfill Artifact caching, given our extension of a non-public API, we've implemented a set of targeted configuration logic. Thus, we're defining a straightforward interface to streamline each Artifact's configuration.

Kotlin
abstract class TaskExtendConfiguration<CreationDataT>(
  val project: Project,
  val variant: Variant,
  var actionList: () -> List<PolyfillAction<CreationDataT>>
) {  
  abstract fun orchestrate()
}

Let's delve deeper into how hook logic functions with AGP Tasks. Notably, for scenarios involving Artifact modifications, timing is of the essence. This timing relevance brings us to the namesake of the configuration processor: TaskExtendConfiguration. Earlier, we referenced manifest-related hooks. Now, let's employ TaskExtendConfiguration to set up an extension configuration. The goal is to retrieve and tweak every submodule of AndroidManifest.xml prior to its merging.

Kotlin
class ManifestMergePreHookConfiguration(
  project: Project,
  private val appVariant: ApplicationVariant,
  actionList: () -> List<PolyfillAction<List<RegularFile>>>
) : MultipleArtifactTaskExtendConfiguration<RegularFile>
  (project, appVariant, actionList) {

  override val data: Provider<List<RegularFile>> = 
    project.objects.newInstance(
    CreateAction::class.java,
    appVariant.getApkCreationConfigImpl().config
      .variantDependencies
      .getArtifactCollection(
        AndroidArtifacts.ConsumedConfigType.RUNTIME_CLASSPATH,
        AndroidArtifacts.ArtifactScope.ALL,
        AndroidArtifacts.ArtifactType.MANIFEST
      )
      .resolvedArtifacts
  ).transform()

  override fun orchestrate() {
    val variantCapitalizedName = variant.getCapitalizedName()
    project.tasks.whenTaskAdded {
      if (this.name == 
        "process${variantCapitalizedName}MainManifest") {
        val localData = data
        actionList().forEachIndexed { index, action ->
          action.onTaskConfigure(this)
          doFirst("ManifestMergePreHookByPolyfill$index") {
            action.onExecute(localData)
          }
        }
      }
    }
  }

  abstract class CreateAction @Inject constructor(
    private val inputCollection: Provider<Set<ResolvedArtifactResult>>
  ) {

    @get:Inject
    abstract val objectFactory: ObjectFactory

    fun transform(): Provider<List<RegularFile>> {
      return inputCollection.map { set ->
        // A workaround to convert File -> RegularFile
        set.map {
          val rp = objectFactory.fileProperty()
          rp.fileValue(it.file)
          rp.get()
        }
      }
    }
  }

}

For comprehending the data retrieval mechanism, consider the segment "Internal Artifact API Exploration" from our discussion in Section 3-5. In orchestrate(), our approach mirrors the search strategy found in Section 3-2. Here, process${variantCapitalizedName}MainManifest is our touchpoint. Our objective is to append its preTaskAction, where actionList mirrors the PolyfillAction processing sequence provided externally via use(...).

In alignment with the Configuration Cache norms, we've integrated two special arrangements:

  • Emulating AGP Task creation, we've incorporated a distinct CreateAction to encapsulate associated logic. This ensures there's no direct reference to ManifestMergePreHookConfiguration and the Project instance.
  • Within orchestrate(), while initiating the doFirst() sequence, we've replicated data to a local variable, localData, to deter any indirect references to both ManifestMergePreHookConfiguration and Project.

To wrap up, transitioning from PolyfilledArtifact to the distinct cache container ArtifactContainer (this container incorporates the initialization of the configuration processor TaskExtendConfiguration, the cache collection of Task Action, and so forth – the full exploration is beyond our scope due to length restrictions), Polyfill leans on the fundamental Map data structure for its mapping. This allows external developers to infuse more tailored Artifact configuration processors.

Kotlin
abstract class PolyfillExtension {
  
  internal val locked = AtomicBoolean(false)
  
  internal val singleArtifactMap = mutableMapOf<PolyfilledArtifact<*>,
      KClass<out TaskExtendConfiguration<*>>>(
    MERGED_RESOURCES to ResourceMergePostHookConfiguration::class
  )
  
    internal val multipleArtifactMap = mutableMapOf<PolyfilledArtifact<*>,
      KClass<out TaskExtendConfiguration<*>>>(
    ALL_MANIFESTS to ManifestMergePreHookConfiguration::class,
    ALL_RESOURCES to ResourceMergePreHookConfiguration::class,
    ALL_JAVA_RES to JavaResourceMergePreHookConfiguration::class
  )
  
  fun registerTaskExtensionConfig(
    artifactType: PolyfilledSingleArtifact<*, *>,
    kClass: KClass<out SingleArtifactTaskExtendConfiguration<*>>
  ) {
    if (locked.get()) {
      return
    }
    singleArtifactMap[artifactType] = kClass
  }

  ...
}

3-6-5: Backward Compatibility: Accommodating Multiple AGP Versions ​

After the initial introduction of the Polyfill's fundamental procedure, there are two supplementary Tasks that must be performed to ease sustained maintenance:

  1. Implement backward compatibility support for AGP.
  2. Enable functional testing across diverse versions.

AGP releases tend to follow a schedule of about 4-6 months for a Minor version (the Minor is a specific term in Semantic Versioning), and 2-3 versions annually. However, it's uncommon for many Android development teams to update each minor version. Given this tendency, it's critical for Polyfill to provide backward compatibility support alongside AGP version updates, which serves to alleviate the strain of user transitions. Assessing the trade-offs, we decide to maintain backward compatibility starting with a single minor version, which suffices to cover user upgrades from the past year.

To engineer the Gradle Plugin for backward compatibility, it's standard practice to leverage the flexibility of Groovy, circumventing less elegant compatibility code such as reflection and if...else... constructs. Moreover, it can also be adapted for the Kotlin project, using the obj.withGroovyBuilder{...} API of Gradle for effortless insertion of Groovy Hook code. A downside of this approach is the necessity to include extra Groovy dependencies, and a lack of code completion when authoring backward support code, adding a degree of complexity to the development and debugging processes.

However, as Polyfill is designed to be compatible with just a single version, we can adopt a more simple strategy - backport patches. We first bifurcate the polyfill-backport module from the core polyfill module, with the former depending on the preceding minor version of AGP (we'll consider 7.1 for this example). Writing code within this module enables us to interact seamlessly with the prior AGP API version and leverage full code completion (while the core module uses 7.2). Next, we introduce an abstract class BackportPatch<Result> to encapsulate the logic for version-dependent code selection that utilizes if...else... constructs.

Kotlin
abstract class BackportPatch<Result> {

  fun applyOrDefault(action: () -> Result): Result {
    val targetVer = SemanticVersionLite(
      AGP_PATCH_IGNORED_VERSION)
    val backportVer = SemanticVersionLite(
      AGP_BACKPORT_PATCH_IGNORED_VERSION)
    val currVer = SemanticVersionLite(
      Version.ANDROID_GRADLE_PLUGIN_VERSION)
    return if (currVer >= backportVer && currVer < targetVer) {
      apply()
    } else { // backportVer > targetVer
      action.invoke()
    }
  }

  abstract fun apply(): Result

}

When a specific resource access exhibits inconsistency between the current and previous minor versions, a compatibility patch inheriting from BackportPath can be crafted. Here's an example:

Kotlin
class ResourceMergePreHookPatch(
  private val mergeTask: MergeResources,
  private val project: Project): BackportPatch<List<Directory>>() {

  override fun apply(): List<Directory> {
    // Get the DependencyResourcesComputer instance via reflection
    val resourcesComputer = ReflectionKit.getField(
      MergeResources::class.java,
      mergeTask,
      "resourcesComputer"
    ) as DependencyResourcesComputer
    
    // Using resourcesComputer.compute(...) to retrieve all resources
    val resourceSets = resourcesComputer.compute(
      mergeTask.processResources, null)
    val resourceFiles = resourceSets.mapNotNull { rs ->
      val getSourceFiles = rs.javaClass.methods.find {
        it.name == "getSourceFiles" && it.parameterCount == 0
      }
      @Suppress("UNCHECKED_CAST")
      getSourceFiles?.invoke(rs) as? Iterable<File>
    }.flatten()
    
    return resourceFiles.map { file ->
      // A workaround to convert File -> RegularFile
      val rp = project.objects.directoryProperty()
      rp.fileValue(file)
      rp.get()
    }
  }

}

Applying compatibility patches in the polyfill main module is straightforward, as demonstrated below:

Kotlin
class ResourceMergePreHookConfiguration(...) : ... {

  override val data: Provider<List<Directory>>
    get() {
      return project.provider {
        val mergeTask = appVariant.getTaskContainer()
            .mergeResourcesTask
            .get()
        
        // A basic use case just like if..else..
        ResourceMergePreHookPatch(mergeTask, project).applyOrDefault {  
          // The default logic for current AGP version
          val resourcesComputer = mergeTask.resourcesComputer
          val resourceSets = resourcesComputer.compute(
            mergeTask.processResources,
            null, 
            project.objects.directoryProperty()
          )
          val resourceFiles = resourceSets.mapNotNull { rs ->
            val getSourceFiles = rs.javaClass.methods.find {
              it.name == "getSourceFiles" && it.parameterCount == 0
            }
            @Suppress("UNCHECKED_CAST")
            getSourceFiles?.invoke(rs) as? Iterable<File>
          }.flatten()
          resourceFiles.map { file ->
            // A workaround to convert File to RegularFile.
            // Not recommended unless necessary.
            val rp = project.objects.directoryProperty()
            rp.fileValue(file)
            rp.get()
          }
        }
      }
    }

  override fun orchestrate() {...}
}

The provided code snippet configures a handler for obtaining all input files for resource merging. A couple of differences between old and new versions are evident:

  • Different approaches for obtaining the ResourcesComputer.
  • Difference in the number of parameters for the compute(...) method.

The benefits of this approach include:

  • Clarity and maintainability of code in both versions, with automated handling of switching logic.
  • When updating to a new minor version, removing all patches and related references from the backport module and then creating a batch of additional patches based on the new version's testing results simplifies the overall process.

This simple solution ensures the seamless integration of Polyfill with AGP. The above Artifact will be used in Section 5-3. In addition, since the test is mentioned, of course, the test of multiple AGP versions should also be added at the end:

Kotlin
companion object {
  @BeforeAll
  @JvmStatic
  fun setup() {
    agpVerProvider().forEach { buildTestProject(it) }
  }
  
  @JvmStatic
  fun agpVerProvider(): List<String> {
    val versions = File("../deps.versions.toml").readText()
    val versionExtractRegex = "..."
    val getVersion = { s: String ->
      versionExtractRegex.format(s)
        .toRegex().find(versions)!!.groupValues[1]
    }
    return listOf(
      getVersion("agpVer"),
      getVersion("agpBackportVer"),
    )
  }
  
  private fun buildTestProject(agpVer: String) {
    ...
    val targetProject = File("./build/test-app-for-$agpVer")
    targetProject.deleteRecursively()
    File(baseTestProjectPath).copyRecursively(targetProject)
    ...      
    GradleRunner.create().apply {
      forwardOutput()
      withProjectDir(targetProject)
      withGradleVersion(...)
      withArguments("clean", "assembleDebug", "--stacktrace")
      withDebug(
        ManagementFactory.getRuntimeMXBean().inputArguments.toString()
          .indexOf("-agentlib:jdwp") > 0
      )
      build()
    }
  }
}

...

@ParameterizedTest
@MethodSource("agpVerProvider")
fun manifestMergePreHookConfigureActionTransformSuccessfully(
  agpVer: String) {
  val out = File("./build/test-app-for-$agpVer/" 
    + "${SampleProjectTest.testProjectJsonOutputPath}"
    + "/all-manifests-from-taskaction2.json"
  )
  assertThat("all-manifests-from-taskaction2.json does't exist",
    out.exists())
  assertThat(out.readText(), StringContains("appcompat"))
  ...
}

Regarding the test code above, we just rely on the standard JUnit framework to process the test. Firstly, we formulated an agpVerProvider function that enables multiple AGP versions for testing. Following that, the required number of test projects were cloned, modifying the AGP version for each project and executing the build. Ultimately, we implemented the @ParameterizedTest and @MethodSource("agpVerProvider") annotations to feed various AGP versions into the test method, thereby repeating the same test routine for all the projects and completing the functional testing for numerous versions. Similar approaches can be adopted for other necessities, like compatibility with various Gradle versions, wherein the parameterized test method can be utilized for injecting the test environment.

Please refer to Section 4-6 for more information on Plugin testing.

3-6-6: Polyfill Integration ​

Shifting our focus from the intricate details, let's reassess the integration process along with the Polyfill features from a user-centric viewpoint to comprehend the aforementioned design.

Firstly, integrate Polyfill into your Gradle Plugin project. It's generally preferable to use a discrete Plugin project or buildSrc for the integration of Polyfill, rather than integrating it directly into the buildscript classpath of the main App project. This is because Polyfill isn't a fully-fledged Gradle Plugin, but it provides increased accessibility to internal Artifacts. Thus, the higher-level Plugin needs to be developed by the user.

kotlin
dependencies {
  compileOnly("com.android.tools.build:gradle:7.2.2")
  implementation("me.2bab:polyfill:0.8.1")  [I]
}

Secondly, invoke the Polyfill Plugin in the higher-level Gradle Plugin's apply(...) function:

Kotlin
import org.gradle.kotlin.dsl.apply

class TestPlugin : Plugin<Project> {
  override fun apply(project: Project) {
    project.apply(plugin = "me.2bab.polyfill")  [II]
    ...
  }
}

As to configure your TaskProvider (only when acquiring Artifact) or PolyfillAction (when modifying Artifact), use the variant.artifactsPolyfill.* related API of Polyfill. This process mirrors the operation of AGP's variant.artifacts:

kotlin
val androidExtension = project.extensions
  .getByType(ApplicationAndroidComponentsExtension::class.java)
androidExtension.onVariants { variant ->

  // get()/getAll()
  val printManifestTask = project.tasks.register<PreUpdateManifestsTask>(
    "getAllInputManifestsFor${variant.name.capitalize()}"
  ) {
    beforeMergeInputs.set(
      variant.artifactsPolyfill
        .getAll(PolyfilledMultipleArtifact.ALL_MANIFESTS)  [III]
    )
  }
  ...

  // use()
  val preHookManifestTaskAction1 = PreUpdateManifestsTaskAction(
    buildDir, id = "preHookManifestTaskAction1")
  variant.artifactsPolyfill.use(
    action = preHookManifestTaskAction1,
    toInPlaceUpdate = PolyfilledMultipleArtifact.ALL_MANIFESTS
  )
}

... 
class PreUpdateManifestsTaskAction(
  buildDir: File,
  id: String
) : PolyfillAction<List<RegularFile>> {

  override fun onTaskConfigure(task: Task) {}

  override fun onExecute(artifact: Provider<List<RegularFile>>) {
    artifact.get().let { files ->
      files.forEach {
        val manifestFile = it.asFile
        ...
      }
    }
  }
  
}

All the artifacts created by the Polyfill are listed below:

PolyfilledSingleArtifactData TypeDescription
MERGED_RESOURCESProvider<Directory>To retrieve the merged /res directory.
PolyfilledMultipleArtifactData TypeDescription
ALL_MANIFESTSListProvider<RegularFile>To retrieve all AndroidManifest.xml regular files that will participate in the merge process.
ALL_RESOURCESListProvider<Directory>To retrieve all /res directories that will participate in the merge process.
ALL_JAVA_RESListProvider<RegularFile>To retrieve all Java Resources that will participate in the merge process.

Moreover, Polyfill supports AGP's Artifact.Single<FILE_TYPE>, Artifact.Multiple<FILE_TYPE>, and their implementations, such as InternalArtifactType, via the get(...)/getAll(...) methods. This allows you to obtain more AGP internal Artifacts.

Kotlin
val getManifestMergeReportTask = project.tasks
  .register<GetManifestMergeReportTask>(
  "getManifestMergeReportFor${variant.name.capitalize()}"
) {
  report.set(
    variant.artifactsPolyfill
      .get(InternalArtifactType.MANIFEST_MERGE_REPORT)  [IV]
  )
  record.set(
    project.objects.fileProperty().fileValue(
      getOutputFile(
        buildDir,
        "manifest-merger-debug-report.txt"
      )
    )
  )
}
project.afterEvaluate {
  variant.getTaskContainer()
    .assembleTask
    .dependsOn(getManifestMergeReportTask)
}

Ultimately, should the above set of APIs fail to meet your requirements, you can employ Polyfill to easily register custom artifacts (Pull Requests are welcome too).

Kotlin
project.extensions.getByType<PolyfillExtension>()
  .registerTaskExtensionConfig(DUMMY_SINGLE_ARTIFACT,
     DummySingleArtifactImpl::class)

For comprehensive feature usage and testing, do visit the ./polyfill-test-plugin and ./functional-test modules in the repository.

3-6-7: In Summary ​

In practical scenarios, while crafting an architecture, developers should strive to keep abreast of current advancements and also anticipate future needs. By creating Polyfill, we can gain access to more artifacts, thereby tackling practical issues, standardizing configuration logic, offering reusable libraries, customizable registration mechanisms, and so on. These capabilities are tailored to alleviate existing challenges and can also be aligned with future AGP development at minimal cost. In Chapter 5, we will delve into how additional Plugins can leverage Polyfill.