Aperture: Introduce gradle task for blueprint generation

This is able to generate whole app/libs tree and update app/Android.bp
according to dependencies in build.gradle.kts.

Change-Id: Ia1a32d29e820bb5d655a3f13688f0160f45a95c7
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 4ed6d03..f54e547 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,3 +1,7 @@
+import groovy.util.Node
+import groovy.util.NodeList
+import groovy.xml.XmlParser
+
 plugins {
     id("com.android.application")
     id("kotlin-android")
@@ -73,3 +77,275 @@
     implementation("io.coil-kt:coil:2.2.0")
     implementation("io.coil-kt:coil-video:2.2.0")
 }
+
+tasks.register("generateBp") {
+    val project = project(":app")
+    val configuration = project.configurations["debugRuntimeClasspath"]
+
+    val libsBase = File("${project.projectDir.absolutePath}/libs")
+    libsBase.deleteRecursively()
+
+    val moduleString = { it: ModuleVersionIdentifier -> "${it.group}:${it.name}:${it.version}" }
+    val modulePath =
+        { it: ModuleVersionIdentifier -> "${it.group.replace(".", "/")}/${it.name}/${it.version}" }
+
+    val spaces = { it: Int ->
+        var ret = ""
+        for (i in it downTo 1) {
+            ret += ' '
+        }
+        ret
+    }
+
+    val moduleName = { it: Any ->
+        when (it) {
+            is ModuleVersionIdentifier -> {
+                "${rootProject.name}_${it.group}_${it.name}"
+            }
+            is String -> {
+                if (it.contains(":")) {
+                    val (group, artifactId) = it.split(":")
+                    "${rootProject.name}_${group}_${artifactId}"
+                } else {
+                    "${rootProject.name}_${it}"
+                }
+            }
+            else -> {
+                throw Exception("Invalid `it` type")
+            }
+        }
+    }
+
+    val moduleNameAosp = { it: String ->
+        when (it) {
+            "androidx.constraintlayout:constraintlayout" -> "androidx-constraintlayout_constraintlayout"
+            "com.google.auto.value:auto-value-annotations" -> "auto_value_annotations"
+            "com.google.guava:listenablefuture" -> "guava"
+            "org.jetbrains.kotlin:kotlin-stdlib" -> "kotlin-stdlib"
+            "org.jetbrains.kotlin:kotlin-stdlib-jdk8" -> "kotlin-stdlib-jdk8"
+            "org.jetbrains.kotlinx:kotlinx-coroutines-android" -> "kotlinx-coroutines-android"
+            else -> it.replace(":", "_")
+        }
+    }
+
+    val isAvailableInAosp = { group: String, artifactId: String ->
+        when {
+            group.startsWith("androidx") -> {
+                // We provide our own androidx.camera & lifecycle-common
+                !group.startsWith("androidx.camera") && artifactId != "lifecycle-common"
+            }
+            group.startsWith("org.jetbrains") -> {
+                // kotlin-android-extensions-runtime & kotlin-parcelize-runtime aren't in AOSP
+                !artifactId.startsWith("kotlin-android-extensions-runtime") &&
+                        !artifactId.startsWith("kotlin-parcelize-runtime")
+            }
+            group == "com.google.auto.value" -> true
+            group == "com.google.guava" -> true
+            group == "junit" -> true
+            else -> false
+        }
+    }
+
+    // Update app/Android.bp
+    File("${project.projectDir.absolutePath}/Android.bp").let { file ->
+        // Read dependencies
+        val dependencies = "${spaces(8)}// DO NOT EDIT THIS SECTION MANUALLY\n".plus(
+            configuration.allDependencies.joinToString("\n") {
+                if (isAvailableInAosp(it.group!!, it.name)) {
+                    "${spaces(8)}\"${moduleNameAosp("${it.group}:${it.name}")}\","
+                } else {
+                    "${spaces(8)}\"${moduleName("${it.group}:${it.name}")}\","
+                }
+            }
+        )
+
+        // Replace existing dependencies with newly generated ones
+        file.writeText(
+            file.readText().replace(
+                "static_libs: \\[.*?\\]".toRegex(RegexOption.DOT_MATCHES_ALL),
+                "static_libs: [%s]".format("\n$dependencies\n${spaces(4)}")
+            )
+        )
+    }
+
+    // Update app/libs
+    configuration.resolvedConfiguration.resolvedArtifacts.sortedBy {
+        moduleString(it.moduleVersion.id)
+    }.distinctBy {
+        moduleString(it.moduleVersion.id)
+    }.forEach {
+        val id = it.moduleVersion.id
+
+        // Skip modules that are available in AOSP
+        if (isAvailableInAosp(id.group, it.name)) {
+            return@forEach
+        }
+
+        // Get file path
+        val dirPath = "${libsBase}/${modulePath(id)}"
+        val filePath = "${dirPath}/${it.file.name}"
+
+        // Copy artifact to app/libs
+        it.file.copyTo(File(filePath))
+
+        // Parse dependencies
+        val dependencies =
+            it.file.parentFile.parentFile.walk().first { file -> file.extension == "pom" }
+                .let { file ->
+                    val ret = mutableListOf<String>()
+
+                    val pom = XmlParser().parse(file)
+                    val dependencies = (pom["dependencies"] as NodeList).firstOrNull() as Node?
+
+                    dependencies?.children()?.forEach { node ->
+                        val dependency = node as Node
+                        ret.add(
+                            "${
+                                (dependency.get("groupId") as NodeList).text()
+                            }:${
+                                (dependency.get("artifactId") as NodeList).text()
+                            }"
+                        )
+                    }
+
+                    ret
+                }
+
+        var targetSdkVersion = 31
+        var minSdkVersion = 14
+
+        // Extract AndroidManifest.xml for AARs
+        if (it.file.extension == "aar") {
+            copy {
+                from(zipTree(filePath).matching { include("/AndroidManifest.xml") }.singleFile)
+                into(dirPath)
+            }
+
+            val androidManifest = XmlParser().parse(File("${dirPath}/AndroidManifest.xml"))
+
+            val usesSdk = (androidManifest["uses-sdk"] as NodeList).first() as Node
+            targetSdkVersion = (usesSdk.get("@targetSdkVersion") as Int?) ?: targetSdkVersion
+            minSdkVersion = (usesSdk.get("@minSdkVersion") as Int?) ?: minSdkVersion
+        }
+
+        // Write Android.bp
+        File("$libsBase/Android.bp").let { file ->
+            // Add autogenerated header if file is empty
+            if (file.length() == 0L) {
+                file.writeText("// DO NOT EDIT THIS FILE MANUALLY")
+            }
+
+            val formatDeps = { addNoDeps: Boolean ->
+                val deps = dependencies.filter { dep ->
+                    when {
+                        configuration.resolvedConfiguration.resolvedArtifacts.firstOrNull { artifact ->
+                            dep == "${artifact.moduleVersion.id.group}:${artifact.moduleVersion.id.name}"
+                        } == null -> {
+                            val moduleName = if (addNoDeps) {
+                                moduleName(id)
+                            } else {
+                                "${moduleName(id)}-nodeps"
+                            }
+                            println("$moduleName: Skipping $dep because it's not in resolvedArtifacts")
+                            false
+                        }
+                        dep == "org.jetbrains.kotlin:kotlin-stdlib-common" -> false
+                        else -> true
+                    }
+
+                }.toMutableList()
+
+                if (addNoDeps) {
+                    // Add -nodeps dependency for android_library/java_library_static
+                    deps.add(0, "${id.group}_${id.name}-nodeps")
+                }
+
+                var ret = ""
+
+                if (deps.isNotEmpty()) {
+                    deps.forEach { dep ->
+                        ret += if (dep.contains(":")) {
+                            val (group, artifactId) = dep.split(":")
+                            if (isAvailableInAosp(group, artifactId)) {
+                                "\n${spaces(8)}\"${moduleNameAosp(dep)}\","
+                            } else {
+                                "\n${spaces(8)}\"${moduleName(dep)}\","
+                            }
+                        } else {
+                            "\n${spaces(8)}\"${moduleName(dep)}\","
+                        }
+                    }
+                    ret += "\n${spaces(4)}"
+                }
+
+                ret
+            }
+
+            when (it.extension) {
+                "aar" -> {
+                    file.appendText(
+                        """
+
+                            android_library_import {
+                                name: "${moduleName(id)}-nodeps",
+                                aars: ["${modulePath(id)}/${it.file.name}"],
+                                sdk_version: "$targetSdkVersion",
+                                min_sdk_version: "$minSdkVersion",
+                                apex_available: [
+                                    "//apex_available:platform",
+                                    "//apex_available:anyapex",
+                                ],
+                                static_libs: [%s],
+                            }
+
+                            android_library {
+                                name: "${moduleName(id)}",
+                                sdk_version: "$targetSdkVersion",
+                                min_sdk_version: "$minSdkVersion",
+                                apex_available: [
+                                    "//apex_available:platform",
+                                    "//apex_available:anyapex",
+                                ],
+                                manifest: "${modulePath(id)}/AndroidManifest.xml",
+                                static_libs: [%s],
+                                java_version: "1.7",
+                            }
+
+                        """.trimIndent().format(formatDeps(false), formatDeps(true))
+                    )
+                }
+                "jar" -> {
+                    file.appendText(
+                        """
+
+                            java_import {
+                                name: "${moduleName(id)}-nodeps",
+                                jars: ["${modulePath(id)}/${it.file.name}"],
+                                sdk_version: "$targetSdkVersion",
+                                min_sdk_version: "$minSdkVersion",
+                                apex_available: [
+                                    "//apex_available:platform",
+                                    "//apex_available:anyapex",
+                                ],
+                            }
+
+                            java_library_static {
+                                name: "${moduleName(id)}",
+                                sdk_version: "$targetSdkVersion",
+                                min_sdk_version: "$minSdkVersion",
+                                apex_available: [
+                                    "//apex_available:platform",
+                                    "//apex_available:anyapex",
+                                ],
+                                static_libs: [%s],
+                                java_version: "1.7",
+                            }
+
+                        """.trimIndent().format(formatDeps(true))
+                    )
+                }
+                else -> throw Exception("Unknown file extension: ${it.extension}")
+            }
+        }
+    }
+}