Avoid using internal AGP APIs in Stable AIDL plugin

Package headers using an AAR transformation, rather than obtaining the
directory from AGP's AidlCompile task.

Obtain the AIDL executable and framework paths by constructing them
ourselves, rather than pulling them from AGP's internal classes.

Bug: 253427301
Test: StableAidlPackageApiTest
Change-Id: Ia7cac47bdef33ab933361e577a38fc349666f85e
diff --git a/stableaidl/stableaidl-gradle-plugin/build.gradle b/stableaidl/stableaidl-gradle-plugin/build.gradle
index 4d56951..9066c1c 100644
--- a/stableaidl/stableaidl-gradle-plugin/build.gradle
+++ b/stableaidl/stableaidl-gradle-plugin/build.gradle
@@ -32,6 +32,7 @@
     implementation(libs.androidToolsRepository)
     implementation(libs.androidToolsSdkCommon)
     implementation(libs.apacheCommonIo)
+    implementation(libs.apacheAnt)
     implementation(libs.guava)
     implementation(libs.kotlinStdlib)
 
diff --git a/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/StableAidlPlugin.kt b/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/StableAidlPlugin.kt
index ea681e8..a0a6848 100644
--- a/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/StableAidlPlugin.kt
+++ b/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/StableAidlPlugin.kt
@@ -17,6 +17,7 @@
 package androidx.stableaidl
 
 import androidx.stableaidl.tasks.StableAidlCompile
+import com.android.build.api.dsl.SdkComponents
 import com.android.build.api.variant.AndroidComponentsExtension
 import com.android.build.api.variant.DslExtension
 import com.android.build.gradle.AppExtension
@@ -27,11 +28,13 @@
 import org.gradle.api.Plugin
 import org.gradle.api.Project
 import org.gradle.api.file.Directory
+import org.gradle.api.file.RegularFile
 import org.gradle.api.provider.Provider
 import org.gradle.api.tasks.TaskProvider
 
-private const val PLUGIN_DIRNAME = "stable-aidl"
+private const val PLUGIN_DIRNAME = "stable_aidl"
 private const val GENERATED_PATH = "generated/source/$PLUGIN_DIRNAME"
+private const val INTERMEDIATES_PATH = "intermediates/${PLUGIN_DIRNAME}_parcelable"
 
 @Suppress("unused", "UnstableApiUsage")
 abstract class StableAidlPlugin : Plugin<Project> {
@@ -41,6 +44,8 @@
             ?: throw GradleException("Stable AIDL plugin requires Android Gradle Plugin")
         val base = project.extensions.getByType(BaseExtension::class.java)
             ?: throw GradleException("Stable AIDL plugin requires Android Gradle Plugin")
+        val aidlExecutable = androidComponents.sdkComponents.aidl(base)
+        val aidlFramework = androidComponents.sdkComponents.aidlFramework(base)
 
         // Extend the android sourceSet.
         androidComponents.registerSourceType(SOURCE_TYPE_STABLE_AIDL)
@@ -74,7 +79,11 @@
         androidComponents.onVariants { variant ->
             val sourceDir = variant.sources.getByName(SOURCE_TYPE_STABLE_AIDL)
             val importsDir = variant.sources.getByName(SOURCE_TYPE_STABLE_AIDL_IMPORTS)
-            val outputDir = project.layout.buildDirectory.dir("$GENERATED_PATH/${variant.name}")
+            val outputDir = project.layout.buildDirectory.dir(
+                "$GENERATED_PATH/${variant.name}")
+            val packagedDir = project.layout.buildDirectory.dir(
+                "$INTERMEDIATES_PATH/${variant.name}/out")
+
             val apiDirName = "$API_DIR/aidl${variant.name.usLocaleCapitalize()}"
             val builtApiDir = project.layout.buildDirectory.dir(apiDirName)
             val lastReleasedApiDir =
@@ -84,16 +93,28 @@
 
             val compileAidlApiTask = registerCompileAidlApi(
                 project,
-                base,
                 variant,
+                aidlExecutable,
+                aidlFramework,
                 sourceDir,
+                packagedDir,
                 importsDir,
                 outputDir
             )
+
+            // To avoid using the same output directory as AGP's AidlCompile task, we need to
+            // register a post-processing task to copy packaged parcelable headers into the AAR.
+            registerPackageAidlApi(
+                project,
+                variant,
+                compileAidlApiTask
+            )
+
             val generateAidlApiTask = registerGenerateAidlApi(
                 project,
-                base,
                 variant,
+                aidlExecutable,
+                aidlFramework,
                 sourceDir,
                 importsDir,
                 builtApiDir,
@@ -101,16 +122,18 @@
             )
             val checkAidlApiReleaseTask = registerCheckApiAidlRelease(
                 project,
-                base,
                 variant,
+                aidlExecutable,
+                aidlFramework,
                 importsDir,
                 lastReleasedApiDir,
                 generateAidlApiTask
             )
             registerCheckAidlApi(
                 project,
-                base,
                 variant,
+                aidlExecutable,
+                aidlFramework,
                 importsDir,
                 lastCheckedInApiDir,
                 generateAidlApiTask,
@@ -172,3 +195,21 @@
         "androidx.stableaidl plugin must be used with Android app, library or feature plugin"
     )
 }
+
+internal fun SdkComponents.aidl(baseExtension: BaseExtension): Provider<RegularFile> =
+    sdkDirectory.map {
+        it.dir("build-tools").dir(baseExtension.buildToolsVersion).file(
+            if (java.lang.System.getProperty("os.name").startsWith("Windows")) {
+                "aidl.exe"
+            } else {
+                "aidl"
+            }
+        )
+    }
+
+internal fun SdkComponents.aidlFramework(baseExtension: BaseExtension): Provider<RegularFile> =
+    sdkDirectory.map {
+        it.dir("platforms")
+            .dir(baseExtension.compileSdkVersion!!)
+            .file("framework.aidl")
+    }
diff --git a/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/StableAidlTasks.kt b/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/StableAidlTasks.kt
index ff8b9ee..733c9eb 100644
--- a/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/StableAidlTasks.kt
+++ b/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/StableAidlTasks.kt
@@ -16,17 +16,19 @@
 
 package androidx.stableaidl
 
+import androidx.stableaidl.tasks.StableAidlPackageApi
 import androidx.stableaidl.tasks.StableAidlCheckApi
 import androidx.stableaidl.tasks.StableAidlCompile
 import androidx.stableaidl.tasks.UpdateStableAidlApiTask
+import com.android.build.api.artifact.SingleArtifact
 import com.android.build.api.variant.SourceDirectories
 import com.android.build.api.variant.Variant
-import com.android.build.gradle.BaseExtension
 import com.android.utils.usLocaleCapitalize
 import java.io.File
 import org.gradle.api.Project
 import org.gradle.api.Task
 import org.gradle.api.file.Directory
+import org.gradle.api.file.RegularFile
 import org.gradle.api.provider.Provider
 import org.gradle.api.tasks.TaskProvider
 
@@ -35,9 +37,11 @@
 @Suppress("UnstableApiUsage") // SourceDirectories.Flat
 fun registerCompileAidlApi(
     project: Project,
-    baseExtension: BaseExtension,
     variant: Variant,
+    aidlExecutable: Provider<RegularFile>,
+    aidlFramework: Provider<RegularFile>,
     sourceDir: SourceDirectories.Flat,
+    packagedDir: Provider<Directory>,
     importsDir: SourceDirectories.Flat,
     outputDir: Provider<Directory>
 ): TaskProvider<StableAidlCompile> = project.tasks.register(
@@ -47,10 +51,11 @@
     task.group = TASK_GROUP_API
     task.description = "Compiles AIDL source code"
     task.variantName = variant.name
-    task.configureBuildToolsFrom(baseExtension)
-    task.configurePackageDirFrom(project, variant)
+    task.aidlExecutable.set(aidlExecutable)
+    task.aidlFrameworkProvider.set(aidlFramework)
     task.sourceDirs.set(sourceDir.all)
     task.sourceOutputDir.set(outputDir)
+    task.packagedDir.set(packagedDir)
     task.importDirs.set(importsDir.all)
     task.extraArgs.set(
         listOf(
@@ -59,11 +64,30 @@
     )
 }
 
+fun registerPackageAidlApi(
+    project: Project,
+    variant: Variant,
+    compileAidlApiTask: TaskProvider<StableAidlCompile>
+): TaskProvider<StableAidlPackageApi> = project.tasks.register(
+    computeTaskName("package", variant, "AidlApi"),
+    StableAidlPackageApi::class.java
+) { task ->
+    task.packagedDir.set(compileAidlApiTask.flatMap { it.packagedDir })
+}.also { taskProvider ->
+    variant.artifacts.use(taskProvider)
+        .wiredWithFiles(
+            StableAidlPackageApi::aarFile,
+            StableAidlPackageApi::updatedAarFile,
+        )
+        .toTransform(SingleArtifact.AAR)
+}
+
 @Suppress("UnstableApiUsage") // SourceDirectories.Flat
 fun registerGenerateAidlApi(
     project: Project,
-    baseExtension: BaseExtension,
     variant: Variant,
+    aidlExecutable: Provider<RegularFile>,
+    aidlFramework: Provider<RegularFile>,
     sourceDir: SourceDirectories.Flat,
     importsDir: SourceDirectories.Flat,
     builtApiDir: Provider<Directory>,
@@ -75,8 +99,8 @@
     task.group = TASK_GROUP_API
     task.description = "Generates API files from AIDL source code"
     task.variantName = variant.name
-    task.configureBuildToolsFrom(baseExtension)
-    task.configurePackageDirFrom(project, variant)
+    task.aidlExecutable.set(aidlExecutable)
+    task.aidlFrameworkProvider.set(aidlFramework)
     task.sourceDirs.set(sourceDir.all)
     task.sourceOutputDir.set(builtApiDir)
     task.importDirs.set(importsDir.all)
@@ -95,8 +119,9 @@
 @Suppress("UnstableApiUsage") // SourceDirectories.Flat
 fun registerCheckApiAidlRelease(
     project: Project,
-    baseExtension: BaseExtension,
     variant: Variant,
+    aidlExecutable: Provider<RegularFile>,
+    aidlFramework: Provider<RegularFile>,
     importsDir: SourceDirectories.Flat,
     lastReleasedApiDir: Directory,
     generateAidlTask: Provider<StableAidlCompile>
@@ -108,7 +133,8 @@
     task.description = "Checks the AIDL source code API surface against the " +
         "stabilized AIDL API files"
     task.variantName = variant.name
-    task.configureBuildToolsFrom(baseExtension)
+    task.aidlExecutable.set(aidlExecutable)
+    task.aidlFrameworkProvider.set(aidlFramework)
     task.importDirs.set(importsDir.all)
     task.checkApiMode.set(StableAidlCheckApi.MODE_COMPATIBLE)
     task.expectedApiDir.set(lastReleasedApiDir)
@@ -123,8 +149,9 @@
 @Suppress("UnstableApiUsage") // SourceDirectories.Flat
 fun registerCheckAidlApi(
     project: Project,
-    baseExtension: BaseExtension,
     variant: Variant,
+    aidlExecutable: Provider<RegularFile>,
+    aidlFramework: Provider<RegularFile>,
     importsDir: SourceDirectories.Flat,
     lastCheckedInApiFile: Directory,
     generateAidlTask: Provider<StableAidlCompile>,
@@ -137,7 +164,8 @@
     task.description = "Checks the AIDL source code API surface against the checked-in " +
         "AIDL API files"
     task.variantName = variant.name
-    task.configureBuildToolsFrom(baseExtension)
+    task.aidlExecutable.set(aidlExecutable)
+    task.aidlFrameworkProvider.set(aidlFramework)
     task.importDirs.set(importsDir.all)
     task.checkApiMode.set(StableAidlCheckApi.MODE_EQUAL)
     task.expectedApiDir.set(lastCheckedInApiFile)
diff --git a/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/tasks/StableAidlCheckApi.kt b/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/tasks/StableAidlCheckApi.kt
index da56750..5b52a95 100644
--- a/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/tasks/StableAidlCheckApi.kt
+++ b/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/tasks/StableAidlCheckApi.kt
@@ -18,9 +18,6 @@
 
 import androidx.stableaidl.internal.LoggerWrapper
 import androidx.stableaidl.internal.process.GradleProcessExecutor
-import com.android.build.gradle.BaseExtension
-import com.android.build.gradle.internal.BuildToolsExecutableInput
-import com.android.build.gradle.internal.services.getBuildService
 import com.android.ide.common.process.LoggedProcessOutputHandler
 import com.google.common.annotations.VisibleForTesting
 import java.io.File
@@ -33,14 +30,12 @@
 import org.gradle.api.file.RegularFileProperty
 import org.gradle.api.provider.ListProperty
 import org.gradle.api.provider.Property
-import org.gradle.api.provider.Provider
 import org.gradle.api.tasks.CacheableTask
 import org.gradle.api.tasks.Input
 import org.gradle.api.tasks.InputDirectory
 import org.gradle.api.tasks.InputFile
 import org.gradle.api.tasks.InputFiles
 import org.gradle.api.tasks.Internal
-import org.gradle.api.tasks.Nested
 import org.gradle.api.tasks.Optional
 import org.gradle.api.tasks.PathSensitive
 import org.gradle.api.tasks.PathSensitivity
@@ -66,10 +61,13 @@
     @get:PathSensitive(PathSensitivity.RELATIVE)
     abstract val importDirs: ListProperty<Directory>
 
-    @InputFile
-    @PathSensitive(PathSensitivity.NONE)
-    fun getAidlFrameworkProvider(): Provider<File> =
-        buildTools.aidlFrameworkProvider()
+    @get:InputFile
+    @get:PathSensitive(PathSensitivity.NONE)
+    abstract val aidlFrameworkProvider: RegularFileProperty
+
+    @get:InputFile
+    @get:PathSensitive(PathSensitivity.NONE)
+    abstract val aidlExecutable: RegularFileProperty
 
     // We cannot use InputDirectory here because the directory may not exist yet.
     @get:InputFiles
@@ -83,9 +81,6 @@
     @get:Input
     abstract val checkApiMode: Property<String>
 
-    @get:Nested
-    abstract val buildTools: BuildToolsExecutableInput
-
     @get:Input
     @get:Optional
     abstract val failOnMissingExpected: Property<Boolean>
@@ -97,23 +92,8 @@
     @get:Inject
     abstract val workerExecutor: WorkerExecutor
 
-    /**
-     * Configures build tools based on AGP's [BaseExtension].
-     */
-    fun configureBuildToolsFrom(baseExtension: BaseExtension) {
-        buildTools.buildToolsRevision.set(baseExtension.buildToolsRevision)
-        buildTools.compileSdkVersion.set(baseExtension.compileSdkVersion)
-        buildTools.sdkBuildService.set(getBuildService(project.gradle.sharedServices))
-    }
-
     @TaskAction
     fun checkApi() {
-        val aidlExecutable = buildTools
-            .aidlExecutableProvider()
-            .get()
-            .absoluteFile
-        val frameworkLocation = getAidlFrameworkProvider().get().absoluteFile
-
         val checkApiMode = checkApiMode.get()
         val expectedApiDir = expectedApiDir.get()
         val actualApiDir = actualApiDir.get()
@@ -133,8 +113,8 @@
 
         aidlCheckApiDelegate(
             workerExecutor,
-            aidlExecutable,
-            frameworkLocation,
+            aidlExecutable.get().asFile,
+            aidlFrameworkProvider.get().asFile,
             extraArgs,
             importDirs.get()
         )
diff --git a/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/tasks/StableAidlCompile.kt b/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/tasks/StableAidlCompile.kt
index dfc62a9..6dd4c09 100644
--- a/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/tasks/StableAidlCompile.kt
+++ b/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/tasks/StableAidlCompile.kt
@@ -19,16 +19,12 @@
 import androidx.stableaidl.internal.DirectoryWalker
 import androidx.stableaidl.internal.LoggerWrapper
 import androidx.stableaidl.internal.process.GradleProcessExecutor
-import com.android.build.api.variant.Variant
-import com.android.build.gradle.BaseExtension
 import com.android.build.gradle.internal.BuildToolsExecutableInput
 import com.android.build.gradle.internal.services.getBuildService
-import com.android.build.gradle.tasks.AidlCompile
 import com.android.builder.compiling.DependencyFileProcessor
 import com.android.builder.internal.incremental.DependencyData
 import com.android.ide.common.process.LoggedProcessOutputHandler
 import com.android.utils.FileUtils
-import com.android.utils.usLocaleCapitalize
 import com.google.common.annotations.VisibleForTesting
 import java.io.File
 import java.io.IOException
@@ -36,20 +32,17 @@
 import java.nio.file.Path
 import javax.inject.Inject
 import org.gradle.api.DefaultTask
-import org.gradle.api.Project
 import org.gradle.api.file.ConfigurableFileCollection
 import org.gradle.api.file.Directory
 import org.gradle.api.file.DirectoryProperty
 import org.gradle.api.file.RegularFileProperty
 import org.gradle.api.provider.ListProperty
 import org.gradle.api.provider.Property
-import org.gradle.api.provider.Provider
 import org.gradle.api.tasks.CacheableTask
 import org.gradle.api.tasks.Input
 import org.gradle.api.tasks.InputFile
 import org.gradle.api.tasks.InputFiles
 import org.gradle.api.tasks.Internal
-import org.gradle.api.tasks.Nested
 import org.gradle.api.tasks.Optional
 import org.gradle.api.tasks.OutputDirectory
 import org.gradle.api.tasks.PathSensitive
@@ -93,10 +86,13 @@
     @get:PathSensitive(PathSensitivity.RELATIVE)
     abstract val importDirs: ListProperty<Directory>
 
-    @InputFile
-    @PathSensitive(PathSensitivity.NONE)
-    fun getAidlFrameworkProvider(): Provider<File> =
-        buildTools.aidlFrameworkProvider()
+    @get:InputFile
+    @get:PathSensitive(PathSensitivity.NONE)
+    abstract val aidlFrameworkProvider: RegularFileProperty
+
+    @get:InputFile
+    @get:PathSensitive(PathSensitivity.NONE)
+    abstract val aidlExecutable: RegularFileProperty
 
     /**
      * Directory for storing AIDL-generated Java sources.
@@ -106,16 +102,11 @@
 
     /**
      * Directory for storing Parcelable headers for consumers.
-     *
-     * These are copied directly from AIDL sources in [sourceDirs].
      */
     @get:OutputDirectory
     @get:Optional
     abstract val packagedDir: DirectoryProperty
 
-    @get:Nested
-    abstract val buildTools: BuildToolsExecutableInput
-
     @get:Input
     @get:Optional
     abstract val extraArgs: ListProperty<String>
@@ -129,40 +120,10 @@
         }
     }
 
-    /**
-     * Configures packaged output directory based on AGP's [AidlCompile] task for the [variant].
-     */
-    fun configurePackageDirFrom(project: Project, variant: Variant) {
-        val compileAidlTask = project.tasks.named(
-            "compile${variant.name.usLocaleCapitalize()}Aidl", AidlCompile::class.java)
-        // Packaged output directory is configured in AGP using:
-        // if (creationConfig.componentType.isAar) {
-        //   creationConfig.artifacts.setInitialProvider(
-        //     taskProvider,
-        //     aidlCompile::packagedDir
-        //   ).withName("out").on(InternalArtifactType.AIDL_PARCELABLE)
-        // }
-        packagedDir.set(compileAidlTask.flatMap { it.packagedDir })
-    }
-
-    /**
-     * Configures build tools based on AGP's [BaseExtension].
-     */
-    fun configureBuildToolsFrom(baseExtension: BaseExtension) {
-        // These are all required by aidlExecutableProvider().
-        buildTools.buildToolsRevision.set(baseExtension.buildToolsRevision)
-        buildTools.compileSdkVersion.set(baseExtension.compileSdkVersion)
-        buildTools.sdkBuildService.set(getBuildService(project.gradle.sharedServices))
-    }
-
     @TaskAction
     fun compile() {
-        // this is full run, clean the previous output'
-        val aidlExecutable = buildTools
-            .aidlExecutableProvider()
-            .get()
-            .absoluteFile
-        val frameworkLocation = getAidlFrameworkProvider().get().absoluteFile
+        // this is full run, clean the previous output
+        // TODO: Is this actually necessary?
         val destinationDir = sourceOutputDir.get().asFile
         FileUtils.cleanOutputDir(destinationDir)
         if (!destinationDir.exists()) {
@@ -179,8 +140,8 @@
 
         aidlCompileDelegate(
             workerExecutor,
-            aidlExecutable,
-            frameworkLocation,
+            aidlExecutable.get().asFile,
+            aidlFrameworkProvider.get().asFile,
             destinationDir,
             parcelableDir?.asFile,
             extraArgs.get(),
diff --git a/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/tasks/StableAidlPackageApi.kt b/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/tasks/StableAidlPackageApi.kt
new file mode 100644
index 0000000..218b300
--- /dev/null
+++ b/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/tasks/StableAidlPackageApi.kt
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * 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 androidx.stableaidl.tasks
+
+import java.io.File
+import java.nio.file.Files
+import org.apache.tools.zip.ZipEntry
+import org.apache.tools.zip.ZipFile
+import org.apache.tools.zip.ZipOutputStream
+import org.gradle.api.DefaultTask
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.file.FileTree
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.tasks.InputDirectory
+import org.gradle.api.tasks.InputFile
+import org.gradle.api.tasks.OutputFile
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.api.tasks.TaskAction
+
+/**
+ * Transforms an AAR by adding parcelable headers.
+ */
+abstract class StableAidlPackageApi : DefaultTask() {
+    @get:InputFile
+    @get:PathSensitive(PathSensitivity.RELATIVE)
+    abstract val aarFile: RegularFileProperty
+
+    @get:InputDirectory
+    @get:PathSensitive(PathSensitivity.RELATIVE)
+    abstract val packagedDir: DirectoryProperty
+
+    @get:OutputFile
+    abstract val updatedAarFile: RegularFileProperty
+
+    @TaskAction
+    fun taskAction() {
+        aidlPackageApiDelegate(
+            aarFile.get().asFile,
+            updatedAarFile.get().asFile,
+            packagedDir.get().asFileTree,
+            name
+        )
+    }
+}
+
+internal fun aidlPackageApiDelegate(
+    aar: File,
+    updatedAar: File,
+    packagedTree: FileTree,
+    name: String,
+) {
+    val tempDir = Files.createTempDirectory("${name}Unzip").toFile()
+    tempDir.deleteOnExit()
+
+    ZipFile(aar).use { aarFile ->
+        aarFile.unzipTo(tempDir)
+    }
+
+    val aidlRoot = File(tempDir, "aidl")
+    if (!aidlRoot.exists()) {
+        aidlRoot.mkdir()
+    }
+
+    // Copy the directory structure and files.
+    packagedTree.visit { details ->
+        val target = File(aidlRoot, details.relativePath.pathString)
+        if (details.isDirectory) {
+            target.mkdir()
+        } else {
+            details.copyTo(target)
+        }
+    }
+
+    tempDir.zipTo(updatedAar)
+    tempDir.deleteRecursively()
+}
+
+internal fun ZipFile.unzipTo(tempDir: File) {
+    entries.iterator().forEach { entry ->
+        if (entry.isDirectory) {
+            File(tempDir, entry.name).mkdirs()
+        } else {
+            val file = File(tempDir, entry.name)
+            file.parentFile.mkdirs()
+            getInputStream(entry).use { stream ->
+                file.writeBytes(stream.readBytes())
+            }
+        }
+    }
+}
+
+internal fun File.zipTo(outZip: File) {
+    ZipOutputStream(outZip.outputStream()).use { stream ->
+        listFiles()!!.forEach { file ->
+            stream.addFileRecursive(null, file)
+        }
+    }
+}
+
+internal fun ZipOutputStream.addFileRecursive(parentPath: String?, file: File) {
+    val entryPath = if (parentPath != null) "$parentPath/${file.name}" else file.name
+    val entry = ZipEntry(file, entryPath)
+
+    // Reset creation time of entry to make it deterministic.
+    entry.time = 0
+    entry.creationTime = java.nio.file.attribute.FileTime.fromMillis(0)
+
+    if (file.isFile) {
+        putNextEntry(entry)
+        file.inputStream().use { stream ->
+            stream.copyTo(this)
+        }
+        closeEntry()
+    } else if (file.isDirectory) {
+        val listFiles = file.listFiles()
+        if (!listFiles.isNullOrEmpty()) {
+            putNextEntry(entry)
+            closeEntry()
+            listFiles.forEach { containedFile ->
+                addFileRecursive(entryPath, containedFile)
+            }
+        }
+    }
+}
diff --git a/stableaidl/stableaidl-gradle-plugin/src/test/java/androidx/stableaidl/tasks/StableAidlCheckApiTest.kt b/stableaidl/stableaidl-gradle-plugin/src/test/java/androidx/stableaidl/tasks/StableAidlCheckApiTest.kt
index 74fd01a..db34a74 100644
--- a/stableaidl/stableaidl-gradle-plugin/src/test/java/androidx/stableaidl/tasks/StableAidlCheckApiTest.kt
+++ b/stableaidl/stableaidl-gradle-plugin/src/test/java/androidx/stableaidl/tasks/StableAidlCheckApiTest.kt
@@ -21,7 +21,6 @@
 import com.android.build.gradle.internal.fixtures.FakeGradleWorkExecutor
 import com.android.build.gradle.internal.fixtures.FakeInjectableService
 import com.google.common.truth.Truth
-import java.io.File
 import kotlin.reflect.jvm.javaMethod
 import org.gradle.api.DefaultTask
 import org.gradle.testfixtures.ProjectBuilder
@@ -43,12 +42,6 @@
     private lateinit var workers: WorkerExecutor
     private lateinit var instantiatorTask: DefaultTask
 
-    private fun createFile(name: String, parent: File): File {
-        val newFile = parent.resolve(name)
-        newFile.createNewFile()
-        return newFile
-    }
-
     @Before
     fun setup() {
         with(ProjectBuilder.builder().withProjectDir(temporaryFolder.newFolder()).build()) {
diff --git a/stableaidl/stableaidl-gradle-plugin/src/test/java/androidx/stableaidl/tasks/StableAidlCompileTest.kt b/stableaidl/stableaidl-gradle-plugin/src/test/java/androidx/stableaidl/tasks/StableAidlCompileTest.kt
index 25c4133..6477c47 100644
--- a/stableaidl/stableaidl-gradle-plugin/src/test/java/androidx/stableaidl/tasks/StableAidlCompileTest.kt
+++ b/stableaidl/stableaidl-gradle-plugin/src/test/java/androidx/stableaidl/tasks/StableAidlCompileTest.kt
@@ -22,7 +22,6 @@
 import com.android.build.gradle.internal.fixtures.FakeGradleWorkExecutor
 import com.android.build.gradle.internal.fixtures.FakeInjectableService
 import com.google.common.truth.Truth
-import java.io.File
 import kotlin.reflect.jvm.javaMethod
 import org.gradle.api.DefaultTask
 import org.gradle.testfixtures.ProjectBuilder
@@ -44,12 +43,6 @@
     private lateinit var workers: WorkerExecutor
     private lateinit var instantiatorTask: DefaultTask
 
-    private fun createFile(name: String, parent: File): File {
-        val newFile = parent.resolve(name)
-        newFile.createNewFile()
-        return newFile
-    }
-
     @Before
     fun setup() {
         with(ProjectBuilder.builder().withProjectDir(temporaryFolder.newFolder()).build()) {
diff --git a/stableaidl/stableaidl-gradle-plugin/src/test/java/androidx/stableaidl/tasks/StableAidlPackageApiTest.kt b/stableaidl/stableaidl-gradle-plugin/src/test/java/androidx/stableaidl/tasks/StableAidlPackageApiTest.kt
new file mode 100644
index 0000000..418bb76
--- /dev/null
+++ b/stableaidl/stableaidl-gradle-plugin/src/test/java/androidx/stableaidl/tasks/StableAidlPackageApiTest.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * 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 androidx.stableaidl.tasks
+
+import java.util.zip.ZipFile
+import kotlin.test.assertContains
+import org.apache.tools.zip.ZipOutputStream
+import org.gradle.testfixtures.ProjectBuilder
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class StableAidlPackageApiTest {
+    @get:Rule
+    val temporaryFolder = TemporaryFolder()
+
+    @Test
+    fun testPackageDirectoryIntoAar() {
+        val aarSourceDir = temporaryFolder.newFolder("aarSourceDir")
+        val aarContents = createFile("AndroidManifest.xml", aarSourceDir)
+
+        val packagedDir = temporaryFolder.newFolder("packagedDir")
+        createFile("android/os/MyParcelable.aidl", packagedDir)
+
+        val outputDir = temporaryFolder.newFolder("outputDir")
+        val aarFile = createFile("aarFile.aar", outputDir)
+        ZipOutputStream(aarFile.outputStream()).use { stream ->
+            stream.addFileRecursive(null, aarContents)
+        }
+        val updatedAarFile = createFile("updatedAarFile.aar", outputDir)
+
+        with(ProjectBuilder.builder().withProjectDir(temporaryFolder.newFolder()).build()) {
+            aidlPackageApiDelegate(
+                aarFile,
+                updatedAarFile,
+                project.fileTree(packagedDir),
+                "test"
+            )
+        }
+
+        ZipFile(updatedAarFile).use { zip ->
+            val zipEntryNames = zip.getEntryNames()
+            assertContains(zipEntryNames, "aidl/android/os/MyParcelable.aidl")
+            assertContains(zipEntryNames, "AndroidManifest.xml")
+        }
+    }
+}
diff --git a/stableaidl/stableaidl-gradle-plugin/src/test/java/androidx/stableaidl/tasks/TestUtils.kt b/stableaidl/stableaidl-gradle-plugin/src/test/java/androidx/stableaidl/tasks/TestUtils.kt
new file mode 100644
index 0000000..dd222cd
--- /dev/null
+++ b/stableaidl/stableaidl-gradle-plugin/src/test/java/androidx/stableaidl/tasks/TestUtils.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * 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 androidx.stableaidl.tasks
+
+import java.io.File
+import java.util.zip.ZipFile
+
+internal fun createFile(name: String, parent: File): File {
+    val newFile = parent.resolve(name)
+    newFile.parentFile.mkdirs()
+    newFile.createNewFile()
+    return newFile
+}
+
+internal fun ZipFile.getEntryNames(): List<String> {
+    val flattened = mutableListOf<String>()
+    entries().iterator().forEach { entry ->
+        flattened += entry.name
+    }
+    return flattened
+}