Initial sdkparcelables

Adds a tool that can convert an SDK stubs jar into a framework.aidl
file by parsing the jar with ASM to find classes that implement
android.os.Parcelable directly or indirectly.

Bug: 70046217
Test: java -cp out/host/linux-x86/framework/sdk_parcelables_test.jar org.junit.runner.JUnitCore com.android.sdk_parcelables.ParcelableDetectorTest
Change-Id: Idc804896b8860352633a85168748af1b08777205
diff --git a/tools/sdkparcelables/Android.bp b/tools/sdkparcelables/Android.bp
new file mode 100644
index 0000000..00fb8aa
--- /dev/null
+++ b/tools/sdkparcelables/Android.bp
@@ -0,0 +1,22 @@
+java_binary_host {
+    name: "sdkparcelables",
+    manifest: "manifest.txt",
+    srcs: [
+        "src/**/*.kt",
+    ],
+    static_libs: [
+        "asm-6.0",
+    ],
+}
+
+java_library_host {
+    name: "sdkparcelables_test",
+    manifest: "manifest.txt",
+    srcs: [
+        "tests/**/*.kt",
+    ],
+    static_libs: [
+        "sdkparcelables",
+        "junit",
+    ],
+}
diff --git a/tools/sdkparcelables/manifest.txt b/tools/sdkparcelables/manifest.txt
new file mode 100644
index 0000000..cd5420c
--- /dev/null
+++ b/tools/sdkparcelables/manifest.txt
@@ -0,0 +1 @@
+Main-class: com.android.sdkparcelables.MainKt
diff --git a/tools/sdkparcelables/src/com/android/sdkparcelables/AncestorCollector.kt b/tools/sdkparcelables/src/com/android/sdkparcelables/AncestorCollector.kt
new file mode 100644
index 0000000..f278aec
--- /dev/null
+++ b/tools/sdkparcelables/src/com/android/sdkparcelables/AncestorCollector.kt
@@ -0,0 +1,28 @@
+package com.android.sdkparcelables
+
+import org.objectweb.asm.ClassVisitor
+import java.util.*
+
+data class Ancestors(val superName: String?, val interfaces: List<String>?)
+
+/** A class that implements an ASM ClassVisitor that collects super class and
+ * implemented interfaces for each class that it visits.
+ */
+class AncestorCollector(api: Int, dest: ClassVisitor?) : ClassVisitor(api, dest) {
+    private val _ancestors = LinkedHashMap<String, Ancestors>()
+
+    val ancestors: Map<String, Ancestors>
+        get() = _ancestors
+
+    override fun visit(version: Int, access: Int, name: String?, signature: String?,
+                       superName: String?, interfaces: Array<out String>?) {
+        name!!
+
+        val old = _ancestors.put(name, Ancestors(superName, interfaces?.toList()))
+        if (old != null) {
+            throw RuntimeException("class $name already found")
+        }
+
+        super.visit(version, access, name, signature, superName, interfaces)
+    }
+}
\ No newline at end of file
diff --git a/tools/sdkparcelables/src/com/android/sdkparcelables/Main.kt b/tools/sdkparcelables/src/com/android/sdkparcelables/Main.kt
new file mode 100644
index 0000000..3e9d92c
--- /dev/null
+++ b/tools/sdkparcelables/src/com/android/sdkparcelables/Main.kt
@@ -0,0 +1,56 @@
+package com.android.sdkparcelables
+
+import org.objectweb.asm.ClassReader
+import org.objectweb.asm.Opcodes
+import java.io.File
+import java.io.IOException
+import java.util.zip.ZipFile
+
+fun main(args: Array<String>) {
+    if (args.size != 2) {
+        usage()
+    }
+
+    val zipFileName = args[0]
+    val aidlFileName = args[1]
+
+    val zipFile: ZipFile
+
+    try {
+        zipFile = ZipFile(zipFileName)
+    } catch (e: IOException) {
+        System.err.println("error reading input jar: ${e.message}")
+        kotlin.system.exitProcess(2)
+    }
+
+    val ancestorCollector = AncestorCollector(Opcodes.ASM6, null)
+
+    for (entry in zipFile.entries()) {
+        if (entry.name.endsWith(".class")) {
+            val reader = ClassReader(zipFile.getInputStream(entry))
+            reader.accept(ancestorCollector,
+                    ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES)
+        }
+    }
+
+    val parcelables = ParcelableDetector.ancestorsToParcelables(ancestorCollector.ancestors)
+
+    try {
+        val outFile = File(aidlFileName)
+        val outWriter = outFile.bufferedWriter()
+        for (parcelable in parcelables) {
+            outWriter.write("parcelable ")
+            outWriter.write(parcelable.replace('/', '.').replace('$', '.'))
+            outWriter.write(";\n")
+        }
+        outWriter.flush()
+    } catch (e: IOException) {
+        System.err.println("error writing output aidl: ${e.message}")
+        kotlin.system.exitProcess(2)
+    }
+}
+
+fun usage() {
+    System.err.println("Usage: <input jar> <output aidl>")
+    kotlin.system.exitProcess(1)
+}
\ No newline at end of file
diff --git a/tools/sdkparcelables/src/com/android/sdkparcelables/ParcelableDetector.kt b/tools/sdkparcelables/src/com/android/sdkparcelables/ParcelableDetector.kt
new file mode 100644
index 0000000..620f798
--- /dev/null
+++ b/tools/sdkparcelables/src/com/android/sdkparcelables/ParcelableDetector.kt
@@ -0,0 +1,52 @@
+package com.android.sdkparcelables
+
+/** A class that uses an ancestor map to find all classes that
+ * implement android.os.Parcelable, including indirectly through
+ * super classes or super interfaces.
+ */
+class ParcelableDetector {
+    companion object {
+        fun ancestorsToParcelables(ancestors: Map<String, Ancestors>): List<String> {
+            val impl = Impl(ancestors)
+            impl.build()
+            return impl.parcelables
+        }
+    }
+
+    private class Impl(val ancestors: Map<String, Ancestors>) {
+        val isParcelableCache = HashMap<String, Boolean>()
+        val parcelables = ArrayList<String>()
+
+        fun build() {
+            val classList = ancestors.keys
+            classList.filterTo(parcelables, this::isParcelable)
+            parcelables.sort()
+        }
+
+        private fun isParcelable(c: String?): Boolean {
+            if (c == null) {
+                return false
+            }
+
+            if (c == "android/os/Parcelable") {
+                return true
+            }
+
+            val old = isParcelableCache[c]
+            if (old != null) {
+                return old
+            }
+
+            val cAncestors = ancestors[c] ?:
+                    throw RuntimeException("class $c missing ancestor information")
+
+            val seq = (cAncestors.interfaces?.asSequence() ?: emptySequence()) +
+                    cAncestors.superName
+
+            val ancestorIsParcelable = seq.any(this::isParcelable)
+
+            isParcelableCache[c] = ancestorIsParcelable
+            return ancestorIsParcelable
+        }
+    }
+}
diff --git a/tools/sdkparcelables/tests/com/android/sdkparcelables/ParcelableDetectorTest.kt b/tools/sdkparcelables/tests/com/android/sdkparcelables/ParcelableDetectorTest.kt
new file mode 100644
index 0000000..edfc825
--- /dev/null
+++ b/tools/sdkparcelables/tests/com/android/sdkparcelables/ParcelableDetectorTest.kt
@@ -0,0 +1,57 @@
+package com.android.sdkparcelables
+
+import junit.framework.TestCase.assertEquals
+import org.junit.Test
+
+class ParcelableDetectorTest {
+    @Test
+    fun `detect implements`() {
+        val ancestorMap = mapOf(
+                testAncestors("android/test/Parcelable",null, "android/os/Parcelable"),
+                testAncestors("android/os/Parcelable", null))
+
+        val parcelables = ParcelableDetector.ancestorsToParcelables(ancestorMap)
+
+        assertEquals(parcelables, listOf("android/os/Parcelable", "android/test/Parcelable"))
+    }
+
+    @Test
+    fun `detect implements in reverse order`() {
+        val ancestorMap = mapOf(
+                testAncestors("android/os/Parcelable", null),
+                testAncestors("android/test/Parcelable",null, "android/os/Parcelable"))
+
+        val parcelables = ParcelableDetector.ancestorsToParcelables(ancestorMap)
+
+        assertEquals(parcelables, listOf("android/os/Parcelable", "android/test/Parcelable"))
+    }
+
+    @Test
+    fun `detect super implements`() {
+        val ancestorMap = mapOf(
+                testAncestors("android/test/SuperParcelable",null, "android/os/Parcelable"),
+                testAncestors("android/test/Parcelable","android/test/SuperParcelable"),
+                testAncestors("android/os/Parcelable", null))
+
+        val parcelables = ParcelableDetector.ancestorsToParcelables(ancestorMap)
+
+        assertEquals(parcelables, listOf("android/os/Parcelable", "android/test/Parcelable", "android/test/SuperParcelable"))
+    }
+
+    @Test
+    fun `detect super interface`() {
+        val ancestorMap = mapOf(
+                testAncestors("android/test/IParcelable",null, "android/os/Parcelable"),
+                testAncestors("android/test/Parcelable",null, "android/test/IParcelable"),
+                testAncestors("android/os/Parcelable", null))
+
+        val parcelables = ParcelableDetector.ancestorsToParcelables(ancestorMap)
+
+        assertEquals(parcelables, listOf("android/os/Parcelable", "android/test/IParcelable", "android/test/Parcelable"))
+    }
+
+}
+
+private fun testAncestors(name: String, superName: String?, vararg interfaces: String): Pair<String, Ancestors> {
+    return Pair(name, Ancestors(superName, interfaces.toList()))
+}