Merge commit '4b8af4810daf8d9998f9101ef123f4e91c58b1c3' into aosp/master

Bug: 281035030
Test: ./gradlew
Change-Id: Ic8042ed88c02f1a072562925eceacd4ed9fa2359
diff --git a/Android.bp b/Android.bp
index d573be9..0606bad 100644
--- a/Android.bp
+++ b/Android.bp
@@ -30,7 +30,7 @@
         "metalava-tools-common-m2-deps",
         "metalava-gradle-plugin-deps",
     ],
-    manifest: "manifest.txt",
+    main_class: "com.android.tools.metalava.Driver",
     target: {
         host: {
             dist: {
diff --git a/OWNERS b/OWNERS
index 29a6ff1..4fdf9f1 100644
--- a/OWNERS
+++ b/OWNERS
@@ -1,7 +1,5 @@
-tnorbye@google.com
-jeffrygaston@google.com
-asfalcone@google.com
-alanv@google.com
-aurimas@google.com
-emberrose@google.com
-sjgilbert@google.com
+amhk@google.com
+gurpreetgs@google.com
+hansson@google.com
+paulduffin@google.com
+jham@google.com #{LAST_RESORT_SUGGESTION}
diff --git a/README.md b/README.md
index 397de8d..3fa0401 100644
--- a/README.md
+++ b/README.md
@@ -351,7 +351,7 @@
 First, metalava provides an ItemVisitor. This lets you visit the API easily.
 For example, here's how you can visit every class:
 
-    coebase.accept(object : ItemVisitor() {
+    codebase.accept(object : ItemVisitor() {
         override fun visitClass(cls: ClassItem) {
             // code operating on the class here
         }
diff --git a/USAGE.md b/USAGE.md
index 9cdc59a..f2fb9fa 100644
--- a/USAGE.md
+++ b/USAGE.md
@@ -26,6 +26,5 @@
 - XML API signature generation (`--api-xml`) for CTS tests and test coverage infrastructure
 - Annotation include, exclude, rewrite, passthrough in stubs (`--include-annotations`, `--exclude-all-annotations`,
 `--pass-through-annotation`, `--exclude-annotation`)
-- Annotation extraction (`--extract-annotations`, `--include-annotation-classes`, `--rewrite-annotations`,
-`--copy-annotations`, `--include-source-retention`) for generating SDK
+- Annotation extraction (`--extract-annotations`, `--copy-annotations`) for generating the SDK
 - Generating SDK metadata (`--sdk-`values`)
diff --git a/build.gradle.kts b/build.gradle.kts
index 28975d3..55684e4 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -99,8 +99,57 @@
     archiveFileName.set("metalava-tests.zip")
 }
 
+fun registerTestPrebuiltsSdkTasks(sourceDir: String, destJar: String): String {
+    val basename = sourceDir.replace("/", "-")
+    val javaCompileTaskName = "$basename.classes"
+    val jarTaskName = "$basename.jar"
+
+    project.tasks.register(javaCompileTaskName, JavaCompile::class) {
+        options.compilerArgs = listOf("--patch-module", "java.base=" + file(sourceDir))
+        source = fileTree(sourceDir)
+        classpath = project.files()
+        destinationDirectory.set(File(getBuildDirectory(), javaCompileTaskName))
+    }
+
+    val dir = destJar.substringBeforeLast("/")
+    val filename = destJar.substringAfterLast("/")
+    if (dir == filename) {
+        throw IllegalArgumentException("bad destJar argument '$destJar'")
+    }
+
+    project.tasks.register(jarTaskName, Jar::class) {
+        from(tasks.named(javaCompileTaskName).get().outputs.files.filter { it.isDirectory })
+        archiveFileName.set(filename)
+        destinationDirectory.set(File(getBuildDirectory(), dir))
+    }
+
+    return jarTaskName
+}
+
+val testPrebuiltsSdkApi30 = registerTestPrebuiltsSdkTasks("src/testdata/prebuilts-sdk-test/30", "prebuilts/sdk/30/public/android.jar")
+val testPrebuiltsSdkApi31 = registerTestPrebuiltsSdkTasks("src/testdata/prebuilts-sdk-test/31", "prebuilts/sdk/31/public/android.jar")
+val testPrebuiltsSdkExt1 = registerTestPrebuiltsSdkTasks("src/testdata/prebuilts-sdk-test/extensions/1", "prebuilts/sdk/extensions/1/public/framework-ext.jar")
+val testPrebuiltsSdkExt2 = registerTestPrebuiltsSdkTasks("src/testdata/prebuilts-sdk-test/extensions/2", "prebuilts/sdk/extensions/2/public/framework-ext.jar")
+val testPrebuiltsSdkExt3 = registerTestPrebuiltsSdkTasks("src/testdata/prebuilts-sdk-test/extensions/3", "prebuilts/sdk/extensions/3/public/framework-ext.jar")
+
+project.tasks.register("test-sdk-extensions-info.xml", Copy::class) {
+    from("src/testdata/prebuilts-sdk-test/sdk-extensions-info.xml")
+    into(File(getBuildDirectory(), "prebuilts/sdk"))
+}
+
+project.tasks.register("test-prebuilts-sdk", Assemble::class) {
+    dependsOn(testPrebuiltsSdkApi30)
+    dependsOn(testPrebuiltsSdkApi31)
+    dependsOn(testPrebuiltsSdkExt1)
+    dependsOn(testPrebuiltsSdkExt2)
+    dependsOn(testPrebuiltsSdkExt3)
+    dependsOn("test-sdk-extensions-info.xml")
+}
+
 val testTask = tasks.named("test", Test::class.java)
 testTask.configure {
+    dependsOn("test-prebuilts-sdk")
+    setEnvironment("METALAVA_TEST_PREBUILTS_SDK_ROOT" to getBuildDirectory().path + "/prebuilts/sdk")
     maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).takeIf { it > 0 } ?: 1
     testLogging.events = hashSetOf(
         TestLogEvent.FAILED,
diff --git a/manifest.txt b/manifest.txt
deleted file mode 100644
index 2128bdd..0000000
--- a/manifest.txt
+++ /dev/null
@@ -1 +0,0 @@
-Main-Class: com.android.tools.metalava.Driver
diff --git a/src/main/java/com/android/tools/metalava/AnnotationsMerger.kt b/src/main/java/com/android/tools/metalava/AnnotationsMerger.kt
index b17091f..0bbabef 100644
--- a/src/main/java/com/android/tools/metalava/AnnotationsMerger.kt
+++ b/src/main/java/com/android/tools/metalava/AnnotationsMerger.kt
@@ -522,7 +522,7 @@
     private fun mergeAnnotations(xmlElement: Element, item: Item) {
         loop@ for (annotationElement in getChildren(xmlElement)) {
             val originalName = getAnnotationName(annotationElement)
-            val qualifiedName = AnnotationItem.mapName(codebase, originalName) ?: originalName
+            val qualifiedName = AnnotationItem.mapName(originalName) ?: originalName
             if (hasNullnessConflicts(item, qualifiedName)) {
                 continue@loop
             }
@@ -801,10 +801,10 @@
     override val originalName: String,
     override val attributes: List<XmlBackedAnnotationAttribute> = emptyList()
 ) : DefaultAnnotationItem(codebase) {
-    override val qualifiedName: String? = AnnotationItem.mapName(codebase, originalName)
+    override val qualifiedName: String? = AnnotationItem.mapName(originalName)
 
     override fun toSource(target: AnnotationTarget, showDefaultAttrs: Boolean): String {
-        val qualifiedName = AnnotationItem.mapName(codebase, qualifiedName, null, target) ?: return ""
+        val qualifiedName = AnnotationItem.mapName(qualifiedName, target) ?: return ""
 
         if (attributes.isEmpty()) {
             return "@$qualifiedName"
diff --git a/src/main/java/com/android/tools/metalava/ApiAnalyzer.kt b/src/main/java/com/android/tools/metalava/ApiAnalyzer.kt
index f1c1753..8c7ecc2 100644
--- a/src/main/java/com/android/tools/metalava/ApiAnalyzer.kt
+++ b/src/main/java/com/android/tools/metalava/ApiAnalyzer.kt
@@ -189,36 +189,22 @@
         }
 
         // Find default constructor, if one doesn't exist
-        val allConstructors = cls.constructors()
-        if (allConstructors.isNotEmpty()) {
+        val constructors = cls.filteredConstructors(filter).toList()
+        if (constructors.isNotEmpty()) {
+            // Try to pick the constructor, select first by fewest throwables,
+            // then fewest parameters, then based on order in listFilter.test(cls)
+            cls.stubConstructor = constructors.reduce { first, second -> pickBest(first, second) }
+            return
+        }
 
-            // Try and use a publicly accessible constructor first.
-            val constructors = cls.filteredConstructors(filter).toList()
-            if (constructors.isNotEmpty()) {
-                // Try to pick the constructor, select first by fewest throwables, then fewest parameters,
-                // then based on order in listFilter.test(cls)
-                cls.stubConstructor = constructors.reduce { first, second -> pickBest(first, second) }
-                return
-            }
-
-            // No accessible constructors are available so one will have to be created, either a private constructor to
-            // prevent instances of the class from being created, or a package private constructor for use by subclasses
-            // in the package to use. Subclasses outside the package would need a protected or public constructor which
-            // would already be part of the API so should have dropped out above.
-            //
-            // The visibility levels on the constructors from the source can give a clue as to what is required. e.g.
-            // if all constructors are private then it is ok for the generated constructor to be private, otherwise it
-            // should be package private.
-            val allPrivate = allConstructors.asSequence()
-                .map { it.isPrivate }
-                .reduce { v1, v2 -> v1 and v2 }
-
-            val visibilityLevel = if (allPrivate) VisibilityLevel.PRIVATE else VisibilityLevel.PACKAGE_PRIVATE
-
-            // No constructors, yet somebody extends this (or private constructor): we have to invent one, such that
-            // subclasses can dispatch to it in the stub files etc
+        // For text based codebase, stub constructor needs to be generated even if
+        // cls.constructors() is empty, so that public default constructor is not created.
+        if (cls.constructors().isNotEmpty() || cls.codebase.preFiltered) {
+            // No accessible constructors are available so a package private constructor is created.
+            // Technically, the stub now has a constructor that isn't available at runtime,
+            // but apps creating subclasses inside the android.* package is not supported.
             cls.stubConstructor = cls.createDefaultConstructor().also {
-                it.mutableModifiers().setVisibilityLevel(visibilityLevel)
+                it.mutableModifiers().setVisibilityLevel(VisibilityLevel.PACKAGE_PRIVATE)
                 it.hidden = false
                 it.superConstructor = superClass?.stubConstructor
             }
@@ -870,7 +856,7 @@
                 if (!method.isConstructor()) {
                     checkTypeReferencesHidden(
                         method,
-                        method.returnType()!!
+                        method.returnType()
                     ) // returnType is nullable only for constructors
                 }
 
@@ -894,7 +880,7 @@
                         method.mutableModifiers().removeAnnotation(it)
                         // Have to also clear the annotation out of the return type itself, if it's a type
                         // use annotation
-                        method.returnType()?.scrubAnnotations()
+                        method.returnType().scrubAnnotations()
                     }
                 }
             }
@@ -1001,7 +987,7 @@
                     }
 
                     val returnType = m.returnType()
-                    if (!m.deprecated && !cl.deprecated && returnType != null && returnType.asClass()?.deprecated == true) {
+                    if (!m.deprecated && !cl.deprecated && returnType.asClass()?.deprecated == true) {
                         reporter.report(
                             Issues.REFERENCES_DEPRECATED, m,
                             "Return type of deprecated type $returnType in ${cl.qualifiedName()}.${m.name()}(): this method should also be deprecated"
@@ -1010,7 +996,7 @@
 
                     var hiddenClass = findHiddenClasses(returnType, stubImportPackages)
                     if (hiddenClass != null && !hiddenClass.isFromClassPath()) {
-                        if (hiddenClass.qualifiedName() == returnType?.asClass()?.qualifiedName()) {
+                        if (hiddenClass.qualifiedName() == returnType.asClass()?.qualifiedName()) {
                             // Return type is hidden
                             reporter.report(
                                 Issues.UNAVAILABLE_SYMBOL, m,
@@ -1057,7 +1043,7 @@
                     }
 
                     val t = m.returnType()
-                    if (t != null && !t.primitive && !m.deprecated && !cl.deprecated && t.asClass()?.deprecated == true) {
+                    if (!t.primitive && !m.deprecated && !cl.deprecated && t.asClass()?.deprecated == true) {
                         reporter.report(
                             Issues.REFERENCES_DEPRECATED, m,
                             "Returning deprecated type $t from ${cl.qualifiedName()}.${m.name()}(): this method should also be deprecated"
@@ -1248,7 +1234,7 @@
                 cantStripThis(thrown, filter, notStrippable, stubImportPackages, method, "as exception")
             }
             val returnType = method.returnType()
-            if (returnType != null && !returnType.primitive) {
+            if (!returnType.primitive) {
                 val returnTypeClass = returnType.asClass()
                 if (returnTypeClass != null) {
                     cantStripThis(returnTypeClass, filter, notStrippable, stubImportPackages, method, "as return type")
diff --git a/src/main/java/com/android/tools/metalava/ApiLint.kt b/src/main/java/com/android/tools/metalava/ApiLint.kt
index 2c96b74..c0df42f 100644
--- a/src/main/java/com/android/tools/metalava/ApiLint.kt
+++ b/src/main/java/com/android/tools/metalava/ApiLint.kt
@@ -243,11 +243,9 @@
     override fun visitMethod(method: MethodItem) {
         checkMethod(method, filterReference)
         val returnType = method.returnType()
-        if (returnType != null) {
-            checkType(returnType, method)
-            checkNullableCollections(returnType, method)
-            checkMethodSuffixListenableFutureReturn(returnType, method)
-        }
+        checkType(returnType, method)
+        checkNullableCollections(returnType, method)
+        checkMethodSuffixListenableFutureReturn(returnType, method)
         for (parameter in method.parameters()) {
             checkType(parameter.type(), parameter)
         }
@@ -579,7 +577,7 @@
             method.parameters().size == 1 &&
                 method.name().startsWith("on") &&
                 !method.parameters().first().type().primitive &&
-                method.returnType()?.toTypeString() == Void.TYPE.name
+                method.returnType().toTypeString() == Void.TYPE.name
 
         if (!methods.all(::isSingleParamCallbackMethod)) return
 
@@ -624,7 +622,6 @@
         val prefix = when (className) {
             "android.content.Intent" -> "android.intent.action"
             "android.provider.Settings" -> "android.settings"
-            "android.app.admin.DevicePolicyManager", "android.app.admin.DeviceAdminReceiver" -> "android.app.action"
             else -> field.containingClass().containingPackage().qualifiedName() + ".action"
         }
         val expected = prefix + "." + name.substring(7)
@@ -661,7 +658,6 @@
         val packageName = field.containingClass().containingPackage().qualifiedName()
         val prefix = when {
             className == "android.content.Intent" -> "android.intent.extra"
-            packageName == "android.app.admin" -> "android.app.extra"
             else -> "$packageName.extra"
         }
         val expected = prefix + "." + name.substring(6)
@@ -919,7 +915,7 @@
     }
 
     private fun checkIntentBuilder(method: MethodItem) {
-        if (method.returnType()?.toTypeString() == "android.content.Intent") {
+        if (method.returnType().toTypeString() == "android.content.Intent") {
             val name = method.name()
             if (name.startsWith("create") && name.endsWith("Intent")) {
                 return
@@ -1056,27 +1052,25 @@
                 )
             } else if (name.startsWith("set") || name.startsWith("add") || name.startsWith("clear")) {
                 val returnType = method.returnType()
-                if (returnType != null) {
-                    val returnsClassType = if (
-                        returnType is PsiTypeItem && clsType is PsiTypeItem
-                    ) {
-                        clsType.isAssignableFromWithoutUnboxing(returnType)
-                    } else {
-                        // fallback to a limited text based check
-                        val returnTypeBounds = returnType
-                            .asTypeParameter(context = method)
-                            ?.typeBounds()?.map {
-                                it.toTypeString()
-                            } ?: emptyList()
-                        returnTypeBounds.contains(clsType.toTypeString()) || returnType == clsType
-                    }
-                    if (!returnsClassType) {
-                        report(
-                            SETTER_RETURNS_THIS, method,
-                            "Methods must return the builder object (return type " +
-                                "$clsType instead of $returnType): ${method.describe()}"
-                        )
-                    }
+                val returnsClassType = if (
+                    returnType is PsiTypeItem && clsType is PsiTypeItem
+                ) {
+                    clsType.isAssignableFromWithoutUnboxing(returnType)
+                } else {
+                    // fallback to a limited text based check
+                    val returnTypeBounds = returnType
+                        .asTypeParameter(context = method)
+                        ?.typeBounds()?.map {
+                            it.toTypeString()
+                        } ?: emptyList()
+                    returnTypeBounds.contains(clsType.toTypeString()) || returnType == clsType
+                }
+                if (!returnsClassType) {
+                    report(
+                        SETTER_RETURNS_THIS, method,
+                        "Methods must return the builder object (return type " +
+                            "$clsType instead of $returnType): ${method.describe()}"
+                    )
                 }
 
                 if (method.modifiers.isNullable()) {
@@ -1249,16 +1243,14 @@
 
         for (method in methodsAndConstructors) {
             val returnType = method.returnType()
-            if (returnType != null) { // not a constructor
-                val returnTypeRank = getTypeRank(returnType)
-                if (returnTypeRank != -1 && returnTypeRank < classRank) {
-                    report(
-                        PACKAGE_LAYERING, cls,
-                        "Method return type `${returnType.toTypeString()}` violates package layering: nothing in `$classPackage` should depend on `${getTypePackage(
-                            returnType
-                        )}`"
-                    )
-                }
+            val returnTypeRank = getTypeRank(returnType)
+            if (returnTypeRank != -1 && returnTypeRank < classRank) {
+                report(
+                    PACKAGE_LAYERING, cls,
+                    "Method return type `${returnType.toTypeString()}` violates package layering: nothing in `$classPackage` should depend on `${getTypePackage(
+                        returnType
+                    )}`"
+                )
             }
 
             for (parameter in method.parameters()) {
@@ -1307,7 +1299,7 @@
         }
 
         fun isGetter(method: MethodItem): Boolean {
-            val returnType = method.returnType() ?: return false
+            val returnType = method.returnType()
             return method.parameters().isEmpty() && returnType.primitive && returnType.toTypeString() == "boolean"
         }
 
@@ -1549,7 +1541,7 @@
             )
         }
         for (method in methods) {
-            if (method.returnType()?.asClass() == cls) {
+            if (method.returnType().asClass() == cls) {
                 report(
                     MANAGER_LOOKUP, method,
                     "Managers must always be obtained from Context (`${method.name()}`)"
@@ -1602,7 +1594,7 @@
                 is MethodItem -> {
                     // For methods requiresNullnessInfo and hasNullnessInfo considers both parameters and return,
                     // only warn about non-annotated returns here as parameters will get visited individually.
-                    if (item.isConstructor() || item.returnType()?.primitive == true) return
+                    if (item.isConstructor() || item.returnType().primitive) return
                     if (item.modifiers.hasNullnessInfo()) return
                     "method `${item.name()}` return"
                 }
@@ -1639,9 +1631,8 @@
 
     private fun anySuperMethodIsNonNull(method: MethodItem): Boolean {
         return method.superMethods().any { superMethod ->
-            superMethod.modifiers.isNonNull() &&
-                // Disable check for generics
-                superMethod.returnType()?.isTypeParameter() != true
+            // Disable check for generics
+            superMethod.modifiers.isNonNull() && !superMethod.returnType().isTypeParameter()
         }
     }
 
@@ -1661,9 +1652,8 @@
 
     private fun anySuperMethodLacksNullnessInfo(method: MethodItem): Boolean {
         return method.superMethods().any { superMethod ->
-            !superMethod.hasNullnessInfo() &&
-                // Disable check for generics
-                superMethod.returnType()?.isTypeParameter() != true
+            // Disable check for generics
+            !superMethod.hasNullnessInfo() && !superMethod.returnType().isTypeParameter()
         }
     }
 
@@ -2131,7 +2121,7 @@
             return
         }
         for (method in methods) {
-            val returnType = method.returnType() ?: continue
+            val returnType = method.returnType()
             if (returnType.primitive) {
                 return
             }
@@ -2171,17 +2161,23 @@
     }
 
     private fun checkUnits(method: MethodItem) {
-        val returnType = method.returnType() ?: return
+        val returnType = method.returnType()
         var type = returnType.toTypeString()
         val name = method.name()
         if (type == "int" || type == "long" || type == "short") {
             if (badUnits.any { name.endsWith(it.key) }) {
-                val badUnit = badUnits.keys.find { name.endsWith(it) }
-                val value = badUnits[badUnit]
-                report(
-                    METHOD_NAME_UNITS, method,
-                    "Expected method name units to be `$value`, was `$badUnit` in `$name`"
-                )
+                val typeIsTypeDef = method.modifiers.annotations().any { annotation ->
+                    val annotationClass = annotation.resolve() ?: return@any false
+                    annotationClass.modifiers.annotations().any { it.isTypeDefAnnotation() }
+                }
+                if (!typeIsTypeDef) {
+                    val badUnit = badUnits.keys.find { name.endsWith(it) }
+                    val value = badUnits[badUnit]
+                    report(
+                        METHOD_NAME_UNITS, method,
+                        "Expected method name units to be `$value`, was `$badUnit` in `$name`"
+                    )
+                }
             }
         } else if (type == "void") {
             if (method.parameters().size != 1) {
@@ -2203,7 +2199,7 @@
     }
 
     private fun checkCloseable(cls: ClassItem, methods: Sequence<MethodItem>) {
-        // AutoClosable has been added in API 19, so libraries with minSdkVersion <19 cannot use it. If the version
+        // AutoCloseable has been added in API 19, so libraries with minSdkVersion <19 cannot use it. If the version
         // is not set, then keep the check enabled.
         val minSdkVersion = codebase.getMinSdkVersion()
         if (minSdkVersion is SetMinSdkVersion && minSdkVersion.value < 19) {
@@ -2220,7 +2216,7 @@
             val foundMethodsDescriptions = foundMethods.joinToString { method -> "${method.name()}()" }
             report(
                 NOT_CLOSEABLE, cls,
-                "Classes that release resources ($foundMethodsDescriptions) should implement AutoClosable and CloseGuard: ${cls.describe()}"
+                "Classes that release resources ($foundMethodsDescriptions) should implement AutoCloseable and CloseGuard: ${cls.describe()}"
             )
         }
     }
@@ -2255,7 +2251,7 @@
                 }
                 // https://kotlinlang.org/docs/reference/operator-overloading.html#increments-and-decrements
                 "inc", "dec" -> {
-                    if (method.parameters().isEmpty() && method.returnType()?.toTypeString() != "void") {
+                    if (method.parameters().isEmpty() && method.returnType().toTypeString() != "void") {
                         flagKotlinOperator(
                             method, "Method can be invoked as a pre/postfix inc/decrement operator from Kotlin: `$name`"
                         )
@@ -2273,7 +2269,7 @@
                     if (methods.any {
                         it.name() == assignName &&
                             it.parameters().size == 1 &&
-                            it.returnType()?.toTypeString() == "void"
+                            it.returnType().toTypeString() == "void"
                     }
                     ) {
                         report(
@@ -2284,7 +2280,7 @@
                 }
                 // https://kotlinlang.org/docs/reference/operator-overloading.html#in
                 "contains" -> {
-                    if (method.parameters().size == 1 && method.returnType()?.toTypeString() == "boolean") {
+                    if (method.parameters().size == 1 && method.returnType().toTypeString() == "boolean") {
                         flagKotlinOperator(
                             method, "Method can be invoked as a \"in\" operator from Kotlin: `$name`"
                         )
@@ -2316,7 +2312,7 @@
                 }
                 // https://kotlinlang.org/docs/reference/operator-overloading.html#assignments
                 "plusAssign", "minusAssign", "timesAssign", "divAssign", "remAssign", "modAssign" -> {
-                    if (method.parameters().size == 1 && method.returnType()?.toTypeString() == "void") {
+                    if (method.parameters().size == 1 && method.returnType().toTypeString() == "void") {
                         flagKotlinOperator(
                             method, "Method can be invoked as a compound assignment operator from Kotlin: `$name`"
                         )
@@ -2369,8 +2365,7 @@
 
     private fun checkUserHandle(cls: ClassItem, methods: Sequence<MethodItem>) {
         val qualifiedName = cls.qualifiedName()
-        if (qualifiedName == "android.app.admin.DeviceAdminReceiver" ||
-            qualifiedName == "android.content.pm.LauncherApps" ||
+        if (qualifiedName == "android.content.pm.LauncherApps" ||
             qualifiedName == "android.os.UserHandle" ||
             qualifiedName == "android.os.UserManager"
         ) {
diff --git a/src/main/java/com/android/tools/metalava/CompatibilityCheck.kt b/src/main/java/com/android/tools/metalava/CompatibilityCheck.kt
index a127c59..89d3645 100644
--- a/src/main/java/com/android/tools/metalava/CompatibilityCheck.kt
+++ b/src/main/java/com/android/tools/metalava/CompatibilityCheck.kt
@@ -56,14 +56,11 @@
      * Request for compatibility checks.
      * [file] represents the signature file to be checked. [apiType] represents which
      * part of the API should be checked, [releaseType] represents what kind of codebase
-     * we are comparing it against. If [codebase] is specified, compare the signature file
-     * against the codebase instead of metalava's current source tree configured via the
-     * normal source path flags.
+     * we are comparing it against.
      */
     data class CheckRequest(
         val file: File,
-        val apiType: ApiType,
-        val codebase: File? = null
+        val apiType: ApiType
     ) {
         override fun toString(): String {
             return "--check-compatibility:${apiType.flagName}:released $file"
@@ -357,7 +354,7 @@
 
         val oldReturnType = old.returnType()
         val newReturnType = new.returnType()
-        if (!new.isConstructor() && oldReturnType != null && newReturnType != null) {
+        if (!new.isConstructor()) {
             val oldTypeParameter = oldReturnType.asTypeParameter(old)
             val newTypeParameter = newReturnType.asTypeParameter(new)
             var compatible = true
@@ -510,6 +507,16 @@
                         new,
                         "${describe(new, capitalize = true)} has added 'final' qualifier"
                     )
+                } else if (old.isEffectivelyFinal() && !new.isEffectivelyFinal()) {
+                    // Disallowed removing final: If an app inherits the class and starts overriding
+                    // the method it's going to crash on earlier versions where the method is final
+                    // It doesn't break compatibility in the strict sense, but does make it very
+                    // difficult to extend this method in practice.
+                    report(
+                        Issues.REMOVED_FINAL,
+                        new,
+                        "${describe(new, capitalize = true)} has removed 'final' qualifier"
+                    )
                 }
             }
         }
@@ -920,26 +927,31 @@
 
     companion object {
         fun checkCompatibility(
-            codebase: Codebase,
-            previous: Codebase,
+            newCodebase: Codebase,
+            oldCodebase: Codebase,
             apiType: ApiType,
-            oldBase: Codebase? = null,
-            newBase: Codebase? = null
+            baseApi: Codebase? = null,
         ) {
             val filter = apiType.getReferenceFilter()
                 .or(apiType.getEmitFilter())
                 .or(ApiType.PUBLIC_API.getReferenceFilter())
                 .or(ApiType.PUBLIC_API.getEmitFilter())
-            val checker = CompatibilityCheck(filter, previous, apiType, newBase, options.reporterCompatibilityReleased)
-            // newBase is considered part of the current codebase
-            val currentFullCodebase = MergedCodebase(listOf(newBase, codebase).filterNotNull())
-            // oldBase is considered part of the previous codebase
-            val previousFullCodebase = MergedCodebase(listOf(oldBase, previous).filterNotNull())
 
-            CodebaseComparator().compare(checker, previousFullCodebase, currentFullCodebase, filter)
+            val checker = CompatibilityCheck(filter, oldCodebase, apiType, baseApi, options.reporterCompatibilityReleased)
+
+            val oldFullCodebase = if (options.showUnannotated && apiType == ApiType.PUBLIC_API) {
+                MergedCodebase(listOfNotNull(oldCodebase, baseApi))
+            } else {
+                // To avoid issues with partial oldCodeBase we fill gaps with newCodebase, the
+                // first parameter is master, so we don't change values of oldCodeBase
+                MergedCodebase(listOfNotNull(oldCodebase, newCodebase))
+            }
+            val newFullCodebase = MergedCodebase(listOfNotNull(newCodebase, baseApi))
+
+            CodebaseComparator().compare(checker, oldFullCodebase, newFullCodebase, filter)
 
             val message = "Found compatibility problems checking " +
-                "the ${apiType.displayName} API (${codebase.location}) against the API in ${previous.location}"
+                "the ${apiType.displayName} API (${newCodebase.location}) against the API in ${oldCodebase.location}"
 
             if (checker.foundProblems) {
                 throw DriverException(exitCode = -1, stderr = message)
diff --git a/src/main/java/com/android/tools/metalava/DexApiWriter.kt b/src/main/java/com/android/tools/metalava/DexApiWriter.kt
index 1142b2e..c2e844e 100644
--- a/src/main/java/com/android/tools/metalava/DexApiWriter.kt
+++ b/src/main/java/com/android/tools/metalava/DexApiWriter.kt
@@ -59,7 +59,7 @@
             writer.print("V")
         } else {
             val returnType = method.returnType()
-            writer.print(returnType?.internalName(method) ?: "V")
+            writer.print(returnType.internalName(method))
         }
         writer.print("\n")
     }
diff --git a/src/main/java/com/android/tools/metalava/DocAnalyzer.kt b/src/main/java/com/android/tools/metalava/DocAnalyzer.kt
index 7e357c0..66f1d32 100644
--- a/src/main/java/com/android/tools/metalava/DocAnalyzer.kt
+++ b/src/main/java/com/android/tools/metalava/DocAnalyzer.kt
@@ -6,6 +6,7 @@
 import com.android.tools.lint.checks.ApiLookup
 import com.android.tools.lint.detector.api.editDistance
 import com.android.tools.lint.helpers.DefaultJavaEvaluator
+import com.android.tools.metalava.apilevels.ApiToExtensionsMap
 import com.android.tools.metalava.model.AnnotationAttributeValue
 import com.android.tools.metalava.model.AnnotationItem
 import com.android.tools.metalava.model.ClassItem
@@ -21,9 +22,14 @@
 import com.intellij.psi.PsiClass
 import com.intellij.psi.PsiField
 import com.intellij.psi.PsiMethod
+import org.w3c.dom.Node
+import org.w3c.dom.NodeList
+import org.xml.sax.Attributes
+import org.xml.sax.helpers.DefaultHandler
 import java.io.File
 import java.nio.file.Files
 import java.util.regex.Pattern
+import javax.xml.parsers.SAXParserFactory
 import kotlin.math.min
 
 /**
@@ -659,12 +665,16 @@
 
     fun applyApiLevels(applyApiLevelsXml: File) {
         val apiLookup = getApiLookup(applyApiLevelsXml)
+        val elementToSdkExtSinceMap = createSymbolToSdkExtSinceMap(applyApiLevelsXml)
 
         val pkgApi = HashMap<PackageItem, Int?>(300)
         codebase.accept(object : ApiVisitor(visitConstructorsAsMethods = true) {
             override fun visitMethod(method: MethodItem) {
                 val psiMethod = method.psi() as? PsiMethod ?: return
                 addApiLevelDocumentation(apiLookup.getMethodVersion(psiMethod), method)
+                elementToSdkExtSinceMap["${psiMethod.containingClass!!.qualifiedName}#${psiMethod.name}"]?.let {
+                    addApiExtensionsDocumentation(it, method)
+                }
                 addDeprecatedDocumentation(apiLookup.getMethodDeprecatedIn(psiMethod), method)
             }
 
@@ -678,12 +688,18 @@
                     val pkg = cls.containingPackage()
                     pkgApi[pkg] = min(pkgApi[pkg] ?: Integer.MAX_VALUE, since)
                 }
+                elementToSdkExtSinceMap["${psiClass.qualifiedName}"]?.let {
+                    addApiExtensionsDocumentation(it, cls)
+                }
                 addDeprecatedDocumentation(apiLookup.getClassDeprecatedIn(psiClass), cls)
             }
 
             override fun visitField(field: FieldItem) {
                 val psiField = field.psi() as PsiField
                 addApiLevelDocumentation(apiLookup.getFieldVersion(psiField), field)
+                elementToSdkExtSinceMap["${psiField.containingClass!!.qualifiedName}#${psiField.name}"]?.let {
+                    addApiExtensionsDocumentation(it, field)
+                }
                 addDeprecatedDocumentation(apiLookup.getFieldDeprecatedIn(psiField), field)
             }
         })
@@ -703,6 +719,13 @@
                 // @SystemApi, @TestApi etc -- don't apply API levels here since we don't have accurate historical data
                 return
             }
+            if (!options.isDeveloperPreviewBuild() && options.currentApiLevel != -1 && level > options.currentApiLevel) {
+                // api-versions.xml currently assigns api+1 to APIs that have not yet been finalized
+                // in a dessert (only in an extension), but for release builds, we don't want to
+                // include a "future" SDK_INT
+                return
+            }
+
             val currentCodeName = options.currentCodeName
             val code: String = if (currentCodeName != null && level > options.currentApiLevel) {
                 currentCodeName
@@ -731,6 +754,24 @@
         }
     }
 
+    private fun addApiExtensionsDocumentation(sdkExtSince: List<SdkAndVersion>, item: Item) {
+        if (item.documentation.contains("@sdkExtSince")) {
+            reporter.report(
+                Issues.FORBIDDEN_TAG, item,
+                "Documentation should not specify @sdkExtSince " +
+                    "manually; it's computed and injected at build time by $PROGRAM_NAME"
+            )
+        }
+        // Don't emit an @sdkExtSince for every item in sdkExtSince; instead, limit output to the
+        // first non-Android SDK listed for the symbol in sdk-extensions-info.txt (the Android SDK
+        // is already covered by @apiSince and doesn't have to be repeated)
+        sdkExtSince.find {
+            it.sdk != ApiToExtensionsMap.ANDROID_PLATFORM_SDK_ID
+        }?.let {
+            item.appendDocumentation("${it.name} ${it.version}", "@sdkExtSince")
+        }
+    }
+
     private fun addDeprecatedDocumentation(level: Int, item: Item) {
         if (level > 0) {
             if (item.originallyHidden) {
@@ -868,3 +909,101 @@
         }
     }
 }
+
+/**
+ * Generate a map of symbol -> (list of SDKs and corresponding versions the symbol first appeared)
+ * in by parsing an api-versions.xml file. This will be used when injecting @sdkExtSince annotations,
+ * which convey the same information, in a format documentation tools can consume.
+ *
+ * A symbol is either of a class, method or field.
+ *
+ * The symbols are Strings on the format "com.pkg.Foo#MethodOrField", with no method signature.
+ */
+private fun createSymbolToSdkExtSinceMap(xmlFile: File): Map<String, List<SdkAndVersion>> {
+    data class OuterClass(val name: String, val idAndVersionList: List<IdAndVersion>?)
+
+    val sdkIdentifiers = mutableMapOf<Int, SdkIdentifier>(
+        ApiToExtensionsMap.ANDROID_PLATFORM_SDK_ID to SdkIdentifier(ApiToExtensionsMap.ANDROID_PLATFORM_SDK_ID, "Android", "Android", "null")
+    )
+    var lastSeenClass: OuterClass? = null
+    val elementToIdAndVersionMap = mutableMapOf<String, List<IdAndVersion>>()
+    val memberTags = listOf("class", "method", "field")
+    val parser = SAXParserFactory.newDefaultInstance().newSAXParser()
+    parser.parse(
+        xmlFile,
+        object : DefaultHandler() {
+            override fun startElement(uri: String, localName: String, qualifiedName: String, attributes: Attributes) {
+                if (qualifiedName == "sdk") {
+                    val id: Int = attributes.getValue("id")?.toIntOrNull() ?: throw IllegalArgumentException("<sdk>: missing or non-integer id attribute")
+                    val shortname: String = attributes.getValue("shortname") ?: throw IllegalArgumentException("<sdk>: missing shortname attribute")
+                    val name: String = attributes.getValue("name") ?: throw IllegalArgumentException("<sdk>: missing name attribute")
+                    val reference: String = attributes.getValue("reference") ?: throw IllegalArgumentException("<sdk>: missing reference attribute")
+                    sdkIdentifiers.put(id, SdkIdentifier(id, shortname, name, reference))
+                } else if (memberTags.contains(qualifiedName)) {
+                    val name: String = attributes.getValue("name") ?: throw IllegalArgumentException("<$qualifiedName>: missing name attribute")
+                    val idAndVersionList: List<IdAndVersion>? = attributes.getValue("sdks")?.split(",")?.map {
+                        val (sdk, version) = it.split(":")
+                        IdAndVersion(sdk.toInt(), version.toInt())
+                    }?.toList()
+
+                    // Populate elementToIdAndVersionMap. The keys constructed here are derived from
+                    // api-versions.xml; when used elsewhere in DocAnalyzer, the keys will be
+                    // derived from PsiItems. The two sources use slightly different nomenclature,
+                    // so change "api-versions.xml nomenclature" to "PsiItems nomenclature" before
+                    // inserting items in the map.
+                    //
+                    // Nomenclature differences:
+                    //   - constructors are named "<init>()V" in api-versions.xml, but
+                    //     "ClassName()V" in PsiItems
+                    //   - inner classes are named "Outer#Inner" in api-versions.xml, but
+                    //     "Outer.Inner" in PsiItems
+                    when (qualifiedName) {
+                        "class" -> {
+                            lastSeenClass = OuterClass(name.replace('/', '.').replace('$', '.'), idAndVersionList)
+                            if (idAndVersionList != null) {
+                                elementToIdAndVersionMap["${lastSeenClass!!.name}"] = idAndVersionList
+                            }
+                        }
+                        "method", "field" -> {
+                            val shortName = if (name.startsWith("<init>")) {
+                                // constructors in api-versions.xml are named '<init>': rename to
+                                // name of class instead, and strip signature: '<init>()V' -> 'Foo'
+                                lastSeenClass!!.name.substringAfterLast('.')
+                            } else {
+                                // strip signature: 'foo()V' -> 'foo'
+                                name.substringBefore('(')
+                            }
+                            val element = "${lastSeenClass!!.name}#$shortName"
+                            if (idAndVersionList != null) {
+                                elementToIdAndVersionMap[element] = idAndVersionList
+                            } else if (lastSeenClass!!.idAndVersionList != null) {
+                                elementToIdAndVersionMap[element] = lastSeenClass!!.idAndVersionList!!
+                            }
+                        }
+                    }
+                }
+            }
+
+            override fun endElement(uri: String, localName: String, qualifiedName: String) {
+                if (qualifiedName == "class") {
+                    lastSeenClass = null
+                }
+            }
+        }
+    )
+
+    val elementToSdkExtSinceMap = mutableMapOf<String, List<SdkAndVersion>>()
+    for (entry in elementToIdAndVersionMap.entries) {
+        elementToSdkExtSinceMap[entry.key] = entry.value.map {
+            val name = sdkIdentifiers.get(it.first)?.name ?: throw IllegalArgumentException("SDK reference to unknown <sdk> with id ${it.first}")
+            SdkAndVersion(it.first, name, it.second)
+        }
+    }
+    return elementToSdkExtSinceMap
+}
+
+private fun NodeList.firstOrNull(): Node? = if (length > 0) { item(0) } else { null }
+
+private typealias IdAndVersion = Pair<Int, Int>
+
+private data class SdkAndVersion(val sdk: Int, val name: String, val version: Int)
diff --git a/src/main/java/com/android/tools/metalava/Driver.kt b/src/main/java/com/android/tools/metalava/Driver.kt
index 57942ec..c4a23a1 100644
--- a/src/main/java/com/android/tools/metalava/Driver.kt
+++ b/src/main/java/com/android/tools/metalava/Driver.kt
@@ -250,8 +250,6 @@
         } else {
             return
         }
-    codebase.apiLevel = options.currentApiLevel +
-        if (options.currentCodeName != null && "REL" != options.currentCodeName) 1 else 0
     options.manifest?.let { codebase.manifest = it }
 
     if (options.verbose) {
@@ -266,8 +264,13 @@
     val androidApiLevelXml = options.generateApiLevelXml
     val apiLevelJars = options.apiLevelJars
     if (androidApiLevelXml != null && apiLevelJars != null) {
+        assert(options.currentApiLevel != -1)
+
         progress("Generating API levels XML descriptor file, ${androidApiLevelXml.name}: ")
-        ApiGenerator.generate(apiLevelJars, options.firstApiLevel, androidApiLevelXml, codebase)
+        ApiGenerator.generate(
+            apiLevelJars, options.firstApiLevel, options.currentApiLevel, options.isDeveloperPreviewBuild(),
+            androidApiLevelXml, codebase, options.sdkJarRoot, options.sdkInfoFile, options.removeMissingClassesInApiLevels
+        )
     }
 
     if (options.docStubsDir != null || options.enhanceDocumentation) {
@@ -385,16 +388,6 @@
             it, codebase, docStubs = false,
             writeStubList = options.stubsSourceList != null
         )
-
-        val stubAnnotations = options.copyStubAnnotationsFrom
-        if (stubAnnotations != null) {
-            // Support pointing to both stub-annotations and stub-annotations/src/main/java
-            val src = File(stubAnnotations, "src${File.separator}main${File.separator}java")
-            val source = if (src.isDirectory) src else stubAnnotations
-            source.listFiles()?.forEach { file ->
-                RewriteAnnotations().copyAnnotations(codebase, file, File(it, file.name))
-            }
-        }
     }
 
     if (options.docStubsDir == null && options.stubsDir == null) {
@@ -455,9 +448,6 @@
         }
     }
 
-    // --rewrite-annotations?
-    options.rewriteAnnotations?.let { RewriteAnnotations().rewriteAnnotations(it) }
-
     // Convert android.jar files?
     options.androidJarSignatureFiles?.let { root ->
         // Generate API signature files for all the historical JAR files
@@ -501,13 +491,13 @@
  * signature file.
  */
 fun checkCompatibility(
-    codebase: Codebase,
+    newCodebase: Codebase,
     check: CheckRequest
 ) {
     progress("Checking API compatibility ($check): ")
     val signatureFile = check.file
 
-    val current =
+    val oldCodebase =
         if (signatureFile.path.endsWith(DOT_JAR)) {
             loadFromJarFile(signatureFile)
         } else {
@@ -517,64 +507,39 @@
             )
         }
 
-    if (current is TextCodebase && current.format > FileFormat.V1 && options.outputFormat == FileFormat.V1) {
-        throw DriverException("Cannot perform compatibility check of signature file $signatureFile in format ${current.format} without analyzing current codebase with $ARG_FORMAT=${current.format}")
+    if (oldCodebase is TextCodebase && oldCodebase.format > FileFormat.V1 && options.outputFormat == FileFormat.V1) {
+        throw DriverException("Cannot perform compatibility check of signature file $signatureFile in format ${oldCodebase.format} without analyzing current codebase with $ARG_FORMAT=${oldCodebase.format}")
     }
 
-    var newBase: Codebase? = null
-    var oldBase: Codebase? = null
+    var baseApi: Codebase? = null
+
     val apiType = check.apiType
 
-    // If diffing with a system-api or test-api (or other signature-based codebase
-    // generated from --show-annotations), the API is partial: it's only listing
-    // the API that is *different* from the base API. This really confuses the
-    // codebase comparison when diffing with a complete codebase, since it looks like
-    // many classes and members have been added and removed. Therefore, the comparison
-    // is simpler if we just make the comparison with the same generated signature
-    // file. If we've only emitted one for the new API, use it directly, if not, generate
-    // it first
-    val new =
-        if (check.codebase != null) {
-            SignatureFileLoader.load(
-                file = check.codebase,
+    if (options.showUnannotated && apiType == ApiType.PUBLIC_API) {
+        // Fast path: if we've already generated a signature file, and it's identical, we're good!
+        val apiFile = options.apiFile
+        if (apiFile != null && apiFile.readText(UTF_8) == signatureFile.readText(UTF_8)) {
+            return
+        }
+        val baseApiFile = options.baseApiForCompatCheck
+        if (baseApiFile != null) {
+            baseApi = SignatureFileLoader.load(
+                file = baseApiFile,
                 kotlinStyleNulls = options.inputKotlinStyleNulls
             )
-        } else if (!options.showUnannotated || apiType != ApiType.PUBLIC_API) {
-            if (options.baseApiForCompatCheck != null) {
-                // This option does not make sense with showAnnotation, as the "base" in that case
-                // is the non-annotated APIs.
-                throw DriverException(
-                    ARG_CHECK_COMPATIBILITY_BASE_API +
-                        " is not compatible with --showAnnotation."
-                )
-            }
-
-            newBase = codebase
-            oldBase = newBase
-
-            codebase
-        } else {
-            // Fast path: if we've already generated a signature file and it's identical, we're good!
-            val apiFile = options.apiFile
-            if (apiFile != null && apiFile.readText(UTF_8) == signatureFile.readText(UTF_8)) {
-                return
-            }
-
-            val baseApiFile = options.baseApiForCompatCheck
-            if (baseApiFile != null) {
-                oldBase = SignatureFileLoader.load(
-                    file = baseApiFile,
-                    kotlinStyleNulls = options.inputKotlinStyleNulls
-                )
-                newBase = oldBase
-            }
-
-            codebase
         }
+    } else if (options.baseApiForCompatCheck != null) {
+        // This option does not make sense with showAnnotation, as the "base" in that case
+        // is the non-annotated APIs.
+        throw DriverException(
+            ARG_CHECK_COMPATIBILITY_BASE_API +
+                " is not compatible with --showAnnotation."
+        )
+    }
 
     // If configured, compares the new API with the previous API and reports
     // any incompatibilities.
-    CompatibilityCheck.checkCompatibility(new, current, apiType, oldBase, newBase)
+    CompatibilityCheck.checkCompatibility(newCodebase, oldCodebase, apiType, baseApi)
 }
 
 fun createTempFile(namePrefix: String, nameSuffix: String): File {
@@ -819,8 +784,15 @@
 }
 
 private fun createStubFiles(stubDir: File, codebase: Codebase, docStubs: Boolean, writeStubList: Boolean) {
-    // Generating stubs from a sig-file-based codebase is problematic
-    assert(codebase.supportsDocumentation())
+    if (codebase is TextCodebase) {
+        if (options.verbose) {
+            options.stdout.println(
+                "Generating stubs from text based codebase is an experimental feature. " +
+                    "It is not guaranteed that stubs generated from text based codebase are " +
+                    "class level equivalent to the stubs generated from source files. "
+            )
+        }
+    }
 
     // Temporary bug workaround for org.chromium.arc
     if (options.sourcePath.firstOrNull()?.path?.endsWith("org.chromium.arc") == true) {
diff --git a/src/main/java/com/android/tools/metalava/ExtractAnnotations.kt b/src/main/java/com/android/tools/metalava/ExtractAnnotations.kt
index c5c7408..4dd3b36 100644
--- a/src/main/java/com/android/tools/metalava/ExtractAnnotations.kt
+++ b/src/main/java/com/android/tools/metalava/ExtractAnnotations.kt
@@ -193,9 +193,7 @@
                 if (annotation.isTypeDefAnnotation()) {
                     // Imported typedef
                     addItem(item, AnnotationHolder(null, annotation, null))
-                } else if (annotation.targets.contains(AnnotationTarget.EXTERNAL_ANNOTATIONS_FILE) &&
-                    !options.includeSourceRetentionAnnotations
-                ) {
+                } else if (annotation.targets.contains(AnnotationTarget.EXTERNAL_ANNOTATIONS_FILE)) {
                     addItem(item, AnnotationHolder(null, annotation, null))
                 }
 
@@ -457,8 +455,8 @@
 
                 if (isConstructor()) {
                     sb.append(escapeXml(containingClass().simpleName()))
-                } else if (returnType() != null) {
-                    sb.append(escapeXml(returnType()!!.toTypeString()))
+                } else {
+                    sb.append(escapeXml(returnType().toTypeString()))
                     sb.append(' ')
                     sb.append(escapeXml(name()))
                 }
diff --git a/src/main/java/com/android/tools/metalava/FileReadSandbox.kt b/src/main/java/com/android/tools/metalava/FileReadSandbox.kt
index 7c5542b..3f88d22 100644
--- a/src/main/java/com/android/tools/metalava/FileReadSandbox.kt
+++ b/src/main/java/com/android/tools/metalava/FileReadSandbox.kt
@@ -13,6 +13,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+// Suppress "SecurityManager is deprecated" warnings: FileReadSandboxTest verifies that the class
+// still works as expected
+@file:Suppress("DEPRECATION")
+
 package com.android.tools.metalava
 
 import java.io.File
diff --git a/src/main/java/com/android/tools/metalava/Issues.kt b/src/main/java/com/android/tools/metalava/Issues.kt
index 1c356fa..903bd62 100644
--- a/src/main/java/com/android/tools/metalava/Issues.kt
+++ b/src/main/java/com/android/tools/metalava/Issues.kt
@@ -127,8 +127,9 @@
     val COMPILE_TIME_CONSTANT = Issue(Severity.ERROR, Category.API_LINT)
     val SINGULAR_CALLBACK = Issue(Severity.ERROR, Category.API_LINT, "callback-class-singular")
     val CALLBACK_NAME = Issue(Severity.WARNING, Category.API_LINT, "observer-should-be-callback")
+    // Obsolete per https://s.android.com/api-guidelines.
     val CALLBACK_INTERFACE =
-        Issue(Severity.ERROR, Category.API_LINT, "callback-abstract-instead-of-interface")
+        Issue(Severity.HIDDEN, Category.API_LINT, "callback-abstract-instead-of-interface")
     val CALLBACK_METHOD_NAME = Issue(Severity.ERROR, Category.API_LINT, "callback-method-naming")
     val LISTENER_INTERFACE = Issue(Severity.ERROR, Category.API_LINT, "callbacks-listener")
     val SINGLE_METHOD_INTERFACE = Issue(Severity.ERROR, Category.API_LINT, "callbacks-listener")
diff --git a/src/main/java/com/android/tools/metalava/JDiffXmlWriter.kt b/src/main/java/com/android/tools/metalava/JDiffXmlWriter.kt
index cb6a114..607b21c 100644
--- a/src/main/java/com/android/tools/metalava/JDiffXmlWriter.kt
+++ b/src/main/java/com/android/tools/metalava/JDiffXmlWriter.kt
@@ -207,7 +207,7 @@
 
         writer.print("<method name=\"")
         writer.print(method.name())
-        method.returnType()?.let {
+        method.returnType().let {
             writer.print("\"\n return=\"")
             writer.print(XmlUtils.toXmlAttributeValue(formatType(it)))
         }
diff --git a/src/main/java/com/android/tools/metalava/NullnessMigration.kt b/src/main/java/com/android/tools/metalava/NullnessMigration.kt
index 77c9a83..3cccb11 100644
--- a/src/main/java/com/android/tools/metalava/NullnessMigration.kt
+++ b/src/main/java/com/android/tools/metalava/NullnessMigration.kt
@@ -50,8 +50,8 @@
     override fun compare(old: MethodItem, new: MethodItem) {
         @Suppress("ConstantConditionIf")
         if (SUPPORT_TYPE_USE_ANNOTATIONS) {
-            val newType = new.returnType() ?: return
-            val oldType = old.returnType() ?: return
+            val newType = new.returnType()
+            val oldType = old.returnType()
             checkType(oldType, newType)
         }
     }
diff --git a/src/main/java/com/android/tools/metalava/Options.kt b/src/main/java/com/android/tools/metalava/Options.kt
index a65542e..4e16fed 100644
--- a/src/main/java/com/android/tools/metalava/Options.kt
+++ b/src/main/java/com/android/tools/metalava/Options.kt
@@ -83,7 +83,6 @@
 const val ARG_HIDE_PACKAGE = "--hide-package"
 const val ARG_MANIFEST = "--manifest"
 const val ARG_MIGRATE_NULLNESS = "--migrate-nullness"
-const val ARG_CHECK_COMPATIBILITY = "--check-compatibility"
 const val ARG_CHECK_COMPATIBILITY_API_RELEASED = "--check-compatibility:api:released"
 const val ARG_CHECK_COMPATIBILITY_REMOVED_RELEASED = "--check-compatibility:removed:released"
 const val ARG_CHECK_COMPATIBILITY_BASE_API = "--check-compatibility:base"
@@ -108,6 +107,7 @@
 const val ARG_HIDE = "--hide"
 const val ARG_APPLY_API_LEVELS = "--apply-api-levels"
 const val ARG_GENERATE_API_LEVELS = "--generate-api-levels"
+const val ARG_REMOVE_MISSING_CLASS_REFERENCES_IN_API_LEVELS = "--remove-missing-class-references-in-api-levels"
 const val ARG_ANDROID_JAR_PATTERN = "--android-jar-pattern"
 const val ARG_CURRENT_VERSION = "--current-version"
 const val ARG_FIRST_VERSION = "--first-version"
@@ -127,8 +127,6 @@
 const val ARG_COMPILE_SDK_VERSION = "--compile-sdk-version"
 const val ARG_INCLUDE_ANNOTATIONS = "--include-annotations"
 const val ARG_COPY_ANNOTATIONS = "--copy-annotations"
-const val ARG_INCLUDE_ANNOTATION_CLASSES = "--include-annotation-classes"
-const val ARG_REWRITE_ANNOTATIONS = "--rewrite-annotations"
 const val ARG_INCLUDE_SOURCE_RETENTION = "--include-source-retention"
 const val ARG_PASS_THROUGH_ANNOTATION = "--pass-through-annotation"
 const val ARG_EXCLUDE_ANNOTATION = "--exclude-annotation"
@@ -160,6 +158,8 @@
 const val ARG_STRICT_INPUT_FILES_WARN = "--strict-input-files:warn"
 const val ARG_STRICT_INPUT_FILES_EXEMPT = "--strict-input-files-exempt"
 const val ARG_REPEAT_ERRORS_MAX = "--repeat-errors-max"
+const val ARG_SDK_JAR_ROOT = "--sdk-extensions-root"
+const val ARG_SDK_INFO_FILE = "--sdk-extensions-info"
 
 class Options(
     private val args: Array<String>,
@@ -252,9 +252,9 @@
 
     /**
      * Whether metalava is invoked as part of updating the API files. When this is true, metalava
-     * should *cancel* various other flags that are also being passed in, such as --check-compatibility.
+     * should *cancel* various other flags that are also being passed in, such as --check-compatibility:*.
      * This is there to ease integration in the build system: for a given target, the build system will
-     * pass all the applicable flags (--stubs, --api, --check-compatibility, --generate-documentation, etc),
+     * pass all the applicable flags (--stubs, --api, --check-compatibility:*, --generate-documentation, etc),
      * and this integration is re-used for the update-api facility where we *only* want to generate the
      * signature files. This avoids having duplicate metalava invocation logic where potentially newly
      * added flags are missing in one of the invocations etc.
@@ -267,7 +267,7 @@
      * files.
      *
      * This is there to ease integration in the build system: for a given target, the build system will
-     * pass all the applicable flags (--stubs, --api, --check-compatibility, --generate-documentation, etc),
+     * pass all the applicable flags (--stubs, --api, --check-compatibility:*, --generate-documentation, etc),
      * and this integration is re-used for the checkapi facility where we *only* want to run compatibility
      * checks. This avoids having duplicate metalava invocation logic where potentially newly
      * added flags are missing in one of the invocations etc.
@@ -356,8 +356,8 @@
 
     /**
      * Annotations that defines APIs that are implicitly included in the API surface. These APIs
-     * will be included in included in certain kinds of output such as stubs, but others (e.g.
-     * API lint and the API signature file) ignore them.
+     * will be included in certain kinds of output such as stubs, but others (e.g. API lint and the
+     * API signature file) ignore them.
      */
     var showForStubPurposesAnnotations: AnnotationFilter = mutableShowForStubPurposesAnnotation
 
@@ -416,22 +416,6 @@
     /** For [ARG_COPY_ANNOTATIONS], the target directory to write converted stub annotations from */
     var privateAnnotationsTarget: File? = null
 
-    /**
-     * For [ARG_INCLUDE_ANNOTATION_CLASSES], the directory to copy stub annotation source files into the
-     * stubs folder from
-     */
-    var copyStubAnnotationsFrom: File? = null
-
-    /**
-     * For [ARG_INCLUDE_SOURCE_RETENTION], true if we want to include source-retention annotations
-     * both in the set of files emitted by [ARG_INCLUDE_ANNOTATION_CLASSES] and into the stubs
-     * themselves
-     */
-    var includeSourceRetentionAnnotations = false
-
-    /** For [ARG_REWRITE_ANNOTATIONS], the jar or bytecode folder to rewrite annotations in */
-    var rewriteAnnotations: List<File>? = null
-
     /** A manifest file to read to for example look up available permissions */
     var manifest: File? = null
 
@@ -498,15 +482,30 @@
      */
     var firstApiLevel = 1
 
-    /** The codename of the codebase, if it's a preview, or null if not specified */
+    /**
+     * The codename of the codebase: non-null string if this is a developer preview build, null if
+     * this is a release build.
+     */
     var currentCodeName: String? = null
 
     /** API level XML file to generate */
     var generateApiLevelXml: File? = null
 
+    /** Whether references to missing classes should be removed from the api levels file. */
+    var removeMissingClassesInApiLevels: Boolean = false
+
     /** Reads API XML file to apply into documentation */
     var applyApiLevelsXml: File? = null
 
+    /** Directory of prebuilt extension SDK jars that contribute to the API */
+    var sdkJarRoot: File? = null
+
+    /**
+     * Rules to filter out some of the extension SDK APIs from the API, and assign extensions to
+     * the APIs that are kept
+     */
+    var sdkInfoFile: File? = null
+
     /** Level to include for javadoc */
     var docLevel = DocLevel.PROTECTED
 
@@ -711,7 +710,6 @@
 
         var androidJarPatterns: MutableList<String>? = null
         var currentJar: File? = null
-        var delayedCheckApiFiles = false
         var skipGenerateAnnotations = false
         reporter = Reporter(null, null)
 
@@ -977,9 +975,6 @@
                     privateAnnotationsSource = stringToExistingDir(getValue(args, ++index))
                     privateAnnotationsTarget = stringToNewDir(getValue(args, ++index))
                 }
-                ARG_REWRITE_ANNOTATIONS -> rewriteAnnotations = stringToExistingDirsOrJars(getValue(args, ++index))
-                ARG_INCLUDE_ANNOTATION_CLASSES -> copyStubAnnotationsFrom = stringToExistingDir(getValue(args, ++index))
-                ARG_INCLUDE_SOURCE_RETENTION -> includeSourceRetentionAnnotations = true
 
                 "--previous-api" -> {
                     migrateNullsFrom = stringToExistingFile(getValue(args, ++index))
@@ -1004,7 +999,7 @@
                     }
                 }
 
-                ARG_CHECK_COMPATIBILITY, ARG_CHECK_COMPATIBILITY_API_RELEASED -> {
+                ARG_CHECK_COMPATIBILITY_API_RELEASED -> {
                     val file = stringToExistingFile(getValue(args, ++index))
                     mutableCompatibilityChecks.add(CheckRequest(file, ApiType.PUBLIC_API))
                 }
@@ -1021,35 +1016,6 @@
 
                 ARG_NO_NATIVE_DIFF -> noNativeDiff = true
 
-                // Compat flag for the old API check command, invoked from build/make/core/definitions.mk:
-                "--check-api-files" -> {
-                    if (index < args.size - 1 && args[index + 1].startsWith("-")) {
-                        // Work around bug where --check-api-files is invoked with all
-                        // the other metalava args before the 4 files; this will be
-                        // fixed by https://android-review.googlesource.com/c/platform/build/+/874473
-                        delayedCheckApiFiles = true
-                    } else {
-                        val stableApiFile = stringToExistingFile(getValue(args, ++index))
-                        val apiFileToBeTested = stringToExistingFile(getValue(args, ++index))
-                        val stableRemovedApiFile = stringToExistingFile(getValue(args, ++index))
-                        val removedApiFileToBeTested = stringToExistingFile(getValue(args, ++index))
-                        mutableCompatibilityChecks.add(
-                            CheckRequest(
-                                stableApiFile,
-                                ApiType.PUBLIC_API,
-                                apiFileToBeTested
-                            )
-                        )
-                        mutableCompatibilityChecks.add(
-                            CheckRequest(
-                                stableRemovedApiFile,
-                                ApiType.REMOVED,
-                                removedApiFileToBeTested
-                            )
-                        )
-                    }
-                }
-
                 ARG_ERROR, "-error" -> setIssueSeverity(
                     getValue(args, ++index),
                     Severity.ERROR,
@@ -1118,7 +1084,10 @@
                     firstApiLevel = Integer.parseInt(getValue(args, ++index))
                 }
                 ARG_CURRENT_CODENAME -> {
-                    currentCodeName = getValue(args, ++index)
+                    val codeName = getValue(args, ++index)
+                    if (codeName != "REL") {
+                        currentCodeName = codeName
+                    }
                 }
                 ARG_CURRENT_JAR -> {
                     currentJar = stringToExistingFile(getValue(args, ++index))
@@ -1135,6 +1104,7 @@
                         stringToExistingFile(getValue(args, ++index))
                     }
                 }
+                ARG_REMOVE_MISSING_CLASS_REFERENCES_IN_API_LEVELS -> removeMissingClassesInApiLevels = true
 
                 ARG_UPDATE_API, "--update-api" -> onlyUpdateApi = true
                 ARG_CHECK_API -> onlyCheckApi = true
@@ -1234,6 +1204,14 @@
                     repeatErrorsMax = Integer.parseInt(getValue(args, ++index))
                 }
 
+                ARG_SDK_JAR_ROOT -> {
+                    sdkJarRoot = stringToExistingDir(getValue(args, ++index))
+                }
+
+                ARG_SDK_INFO_FILE -> {
+                    sdkInfoFile = stringToExistingFile(getValue(args, ++index))
+                }
+
                 "--temp-folder" -> {
                     tempFolder = stringToNewOrExistingDir(getValue(args, ++index))
                 }
@@ -1391,35 +1369,13 @@
                         val usage = getUsage(includeHeader = false, colorize = color)
                         throw DriverException(stderr = "Invalid argument $arg\n\n$usage")
                     } else {
-                        if (delayedCheckApiFiles) {
-                            delayedCheckApiFiles = false
-                            val stableApiFile = stringToExistingFile(arg)
-                            val apiFileToBeTested = stringToExistingFile(getValue(args, ++index))
-                            val stableRemovedApiFile = stringToExistingFile(getValue(args, ++index))
-                            val removedApiFileToBeTested = stringToExistingFile(getValue(args, ++index))
-                            mutableCompatibilityChecks.add(
-                                CheckRequest(
-                                    stableApiFile,
-                                    ApiType.PUBLIC_API,
-                                    apiFileToBeTested
-                                )
-                            )
-                            mutableCompatibilityChecks.add(
-                                CheckRequest(
-                                    stableRemovedApiFile,
-                                    ApiType.REMOVED,
-                                    removedApiFileToBeTested
-                                )
-                            )
-                        } else {
-                            // All args that don't start with "-" are taken to be filenames
-                            mutableSources.addAll(stringToExistingFiles(arg))
+                        // All args that don't start with "-" are taken to be filenames
+                        mutableSources.addAll(stringToExistingFiles(arg))
 
-                            // Temporary workaround for
-                            // aosp/I73ff403bfc3d9dfec71789a3e90f9f4ea95eabe3
-                            if (arg.endsWith("hwbinder-stubs-docs-stubs.srcjar.rsp")) {
-                                skipGenerateAnnotations = true
-                            }
+                        // Temporary workaround for
+                        // aosp/I73ff403bfc3d9dfec71789a3e90f9f4ea95eabe3
+                        if (arg.endsWith("hwbinder-stubs-docs-stubs.srcjar.rsp")) {
+                            skipGenerateAnnotations = true
                         }
                     }
                 }
@@ -1429,6 +1385,10 @@
         }
 
         if (generateApiLevelXml != null) {
+            if (currentApiLevel == -1) {
+                throw DriverException(stderr = "$ARG_GENERATE_API_LEVELS requires $ARG_CURRENT_VERSION")
+            }
+
             // <String> is redundant here but while IDE (with newer type inference engine
             // understands that) the current 1.3.x compiler does not
             @Suppress("RemoveExplicitTypeArguments")
@@ -1441,12 +1401,15 @@
             apiLevelJars = findAndroidJars(
                 patterns,
                 firstApiLevel,
-                currentApiLevel,
-                currentCodeName,
+                currentApiLevel + if (isDeveloperPreviewBuild()) 1 else 0,
                 currentJar
             )
         }
 
+        if ((sdkJarRoot == null) != (sdkInfoFile == null)) {
+            throw DriverException(stderr = "$ARG_SDK_JAR_ROOT and $ARG_SDK_INFO_FILE must both be supplied")
+        }
+
         // outputKotlinStyleNulls implies at least format=v3
         if (outputKotlinStyleNulls) {
             if (outputFormat < FileFormat.V3) {
@@ -1473,6 +1436,8 @@
             // flags count
             apiLevelJars = null
             generateApiLevelXml = null
+            sdkJarRoot = null
+            sdkInfoFile = null
             applyApiLevelsXml = null
             androidJarSignatureFiles = null
             stubsDir = null
@@ -1493,6 +1458,8 @@
         } else if (onlyCheckApi) {
             apiLevelJars = null
             generateApiLevelXml = null
+            sdkJarRoot = null
+            sdkInfoFile = null
             applyApiLevelsXml = null
             androidJarSignatureFiles = null
             stubsDir = null
@@ -1562,6 +1529,8 @@
         checkFlagConsistency()
     }
 
+    public fun isDeveloperPreviewBuild(): Boolean = currentCodeName != null
+
     /** Update the classpath to insert android.jar or JDK classpath elements if necessary */
     private fun updateClassPath() {
         val sdkHome = sdkHome
@@ -1637,17 +1606,8 @@
         androidJarPatterns: List<String>,
         minApi: Int,
         currentApiLevel: Int,
-        currentCodeName: String?,
         currentJar: File?
     ): Array<File> {
-
-        @Suppress("NAME_SHADOWING")
-        val currentApiLevel = if (currentCodeName != null && "REL" != currentCodeName) {
-            currentApiLevel + 1
-        } else {
-            currentApiLevel
-        }
-
         val apiLevelFiles = mutableListOf<File>()
         // api level 0: placeholder, should not be processed.
         // (This is here because we want the array index to match
@@ -2160,13 +2120,9 @@
             "Whether the signature file being read should be " +
                 "interpreted as having encoded its types using Kotlin style types: a suffix of \"?\" for nullable " +
                 "types, no suffix for non nullable types, and \"!\" for unknown. The default is no.",
-            "$ARG_CHECK_COMPATIBILITY:type:state <file>",
+            "--check-compatibility:type:released <file>",
             "Check compatibility. Type is one of 'api' " +
-                "and 'removed', which checks either the public api or the removed api. State is one of " +
-                "'current' and 'released', to check either the currently in development API or the last publicly " +
-                "released API, respectively. Different compatibility checks apply in the two scenarios. " +
-                "For example, to check the code base against the current public API, use " +
-                "$ARG_CHECK_COMPATIBILITY:api:current.",
+                "and 'removed', which checks either the public api or the removed api.",
             "$ARG_CHECK_COMPATIBILITY_BASE_API <file>",
             "When performing a compat check, use the provided signature " +
                 "file as a base api, which is treated as part of the API being checked. This allows us to compute the " +
@@ -2235,12 +2191,6 @@
             "$ARG_EXTRACT_ANNOTATIONS <zipfile>",
             "Extracts source annotations from the source files and writes " +
                 "them into the given zip file",
-            "$ARG_INCLUDE_ANNOTATION_CLASSES <dir>",
-            "Copies the given stub annotation source files into the " +
-                "generated stub sources; <dir> is typically $PROGRAM_NAME/stub-annotations/src/main/java/.",
-            "$ARG_REWRITE_ANNOTATIONS <dir/jar>",
-            "For a bytecode folder or output jar, rewrites the " +
-                "androidx annotations to be package private",
             "$ARG_FORCE_CONVERT_TO_WARNING_NULLABILITY_ANNOTATIONS <package1:-package2:...>",
             "On every API declared " +
                 "in a class referenced by the given filter, makes nullability issues appear to callers as warnings " +
@@ -2262,6 +2212,11 @@
             "$ARG_GENERATE_API_LEVELS <xmlfile>",
             "Reads android.jar SDK files and generates an XML file recording " +
                 "the API level for each class, method and field",
+            "$ARG_REMOVE_MISSING_CLASS_REFERENCES_IN_API_LEVELS",
+            "Removes references to missing classes when generating the API levels XML file. " +
+                "This can happen when generating the XML file for the non-updatable portions of " +
+                "the module-lib sdk, as those non-updatable portions can reference classes that are " +
+                "part of an updatable apex.",
             "$ARG_ANDROID_JAR_PATTERN <pattern>",
             "Patterns to use to locate Android JAR files. The default " +
                 "is \$ANDROID_HOME/platforms/android-%/android.jar.",
@@ -2269,6 +2224,28 @@
             ARG_CURRENT_VERSION, "Sets the current API level of the current source code",
             ARG_CURRENT_CODENAME, "Sets the code name for the current source code",
             ARG_CURRENT_JAR, "Points to the current API jar, if any",
+            ARG_SDK_JAR_ROOT,
+            "Points to root of prebuilt extension SDK jars, if any. This directory is expected to " +
+                "contain snapshots of historical extension SDK versions in the form of stub jars. " +
+                "The paths should be on the format \"<int>/public/<module-name>.jar\", where <int> " +
+                "corresponds to the extension SDK version, and <module-name> to the name of the mainline module.",
+            ARG_SDK_INFO_FILE,
+            "Points to map of extension SDK APIs to include, if any. The file is a plain text file " +
+                "and describes, per extension SDK, what APIs from that extension to include in the " +
+                "file created via $ARG_GENERATE_API_LEVELS. The format of each line is one of the following: " +
+                "\"<module-name> <pattern> <ext-name> [<ext-name> [...]]\", where <module-name> is the " +
+                "name of the mainline module this line refers to, <pattern> is a common Java name prefix " +
+                "of the APIs this line refers to, and <ext-name> is a list of extension SDK names " +
+                "in which these SDKs first appeared, or \"<ext-name> <ext-id> <type>\", where " +
+                "<ext-name> is the name of an SDK, " +
+                "<ext-id> its numerical ID and <type> is one of " +
+                "\"platform\" (the Android platform SDK), " +
+                "\"platform-ext\" (an extension to the Android platform SDK), " +
+                "\"standalone\" (a separate SDK). " +
+                "Fields are separated by whitespace. " +
+                "A mainline module may be listed multiple times. " +
+                "The special pattern \"*\" refers to all APIs in the given mainline module. " +
+                "Lines beginning with # are comments.",
 
             "", "\nSandboxing:",
             ARG_NO_IMPLICIT_ROOT,
diff --git a/src/main/java/com/android/tools/metalava/RewriteAnnotations.kt b/src/main/java/com/android/tools/metalava/RewriteAnnotations.kt
index a34c162..a251b8e 100644
--- a/src/main/java/com/android/tools/metalava/RewriteAnnotations.kt
+++ b/src/main/java/com/android/tools/metalava/RewriteAnnotations.kt
@@ -17,26 +17,9 @@
 package com.android.tools.metalava
 
 import com.android.SdkConstants
-import com.android.SdkConstants.DOT_CLASS
-import com.android.SdkConstants.DOT_JAR
 import com.android.tools.metalava.model.AnnotationRetention
 import com.android.tools.metalava.model.Codebase
-import com.google.common.io.Closer
-import org.objectweb.asm.ClassReader
-import org.objectweb.asm.ClassVisitor
-import org.objectweb.asm.ClassWriter
-import org.objectweb.asm.Opcodes
-import org.objectweb.asm.Opcodes.ASM6
-import java.io.BufferedInputStream
-import java.io.BufferedOutputStream
 import java.io.File
-import java.io.FileInputStream
-import java.io.FileOutputStream
-import java.io.IOException
-import java.nio.file.attribute.FileTime
-import java.util.jar.JarEntry
-import java.util.zip.ZipInputStream
-import java.util.zip.ZipOutputStream
 import kotlin.text.Charsets.UTF_8
 
 /**
@@ -51,12 +34,10 @@
     fun modifyAnnotationSources(codebase: Codebase?, source: File, target: File, pkg: String = "") {
         val fileName = source.name
         if (fileName.endsWith(SdkConstants.DOT_JAVA)) {
-            if (!options.includeSourceRetentionAnnotations) {
-                // Only copy non-source retention annotation classes
-                val qualifiedName = pkg + "." + fileName.substring(0, fileName.indexOf('.'))
-                if (hasSourceRetention(codebase, qualifiedName)) {
-                    return
-                }
+            // Only copy non-source retention annotation classes
+            val qualifiedName = pkg + "." + fileName.substring(0, fileName.indexOf('.'))
+            if (hasSourceRetention(codebase, qualifiedName)) {
+                return
             }
 
             // Copy and convert
@@ -75,49 +56,6 @@
         }
     }
 
-    /** Copies annotation source files from [source] to [target] */
-    fun copyAnnotations(codebase: Codebase, source: File, target: File, pkg: String = "") {
-        val fileName = source.name
-        if (fileName.endsWith(SdkConstants.DOT_JAVA)) {
-            if (!options.includeSourceRetentionAnnotations) {
-                // Only copy non-source retention annotation classes
-                val qualifiedName = pkg + "." + fileName.substring(0, fileName.indexOf('.'))
-                if (hasSourceRetention(codebase, qualifiedName)) {
-                    return
-                }
-            }
-
-            // Copy and convert
-            target.parentFile.mkdirs()
-            source.copyTo(target)
-        } else if (source.isDirectory) {
-            val newPackage = if (pkg.isEmpty()) fileName else "$pkg.$fileName"
-            source.listFiles()?.forEach {
-                copyAnnotations(codebase, it, File(target, it.name), newPackage)
-            }
-        }
-    }
-
-    /** Writes the bytecode for the compiled annotations in the given file list such that they are package private */
-    fun rewriteAnnotations(files: List<File>) {
-        for (file in files) {
-            // Jump directly into androidx/annotation if it appears we were invoked at the top level
-            if (file.isDirectory) {
-                val android = File(file, "android${File.separator}annotation/")
-                if (android.isDirectory) {
-                    rewriteAnnotations(android)
-                    val androidx = File(file, "androidx${File.separator}annotation/")
-                    if (androidx.isDirectory) {
-                        rewriteAnnotations(androidx)
-                    }
-                    continue
-                }
-            }
-
-            rewriteAnnotations(file)
-        }
-    }
-
     /** Returns true if the given annotation class name has source retention as far as the stub
      * annotations are concerned.
      */
@@ -139,129 +77,4 @@
             error("Found annotation with unknown desired retention: " + qualifiedName)
         }
     }
-
-    /** Writes the bytecode for the compiled annotations in the given file such that they are package private */
-    private fun rewriteAnnotations(file: File) {
-        when {
-            file.isDirectory -> file.listFiles()?.forEach { rewriteAnnotations(it) }
-            file.path.endsWith(DOT_CLASS) -> rewriteClassFile(file)
-            file.path.endsWith(DOT_JAR) -> rewriteJar(file)
-        }
-    }
-
-    private fun rewriteClassFile(file: File) {
-        if (file.name.contains("$")) {
-            return // Not worrying about inner classes
-        }
-        val bytes = file.readBytes()
-        val rewritten = rewriteClass(bytes, file.path) ?: return
-        file.writeBytes(rewritten)
-    }
-
-    private fun rewriteClass(bytes: ByteArray, path: String): ByteArray? {
-        return try {
-            val reader = ClassReader(bytes)
-            rewriteOuterClass(reader)
-        } catch (ioe: IOException) {
-            error("Could not process " + path + ": " + ioe.localizedMessage)
-        }
-    }
-
-    private fun rewriteOuterClass(reader: ClassReader): ByteArray? {
-        val classWriter = ClassWriter(ASM6)
-        var skip = true
-        val classVisitor = object : ClassVisitor(ASM6, classWriter) {
-            override fun visit(
-                version: Int,
-                access: Int,
-                name: String,
-                signature: String?,
-                superName: String?,
-                interfaces: Array<out String>?
-            ) {
-                // Only process public annotations in android.annotation and androidx.annotation
-                if (access and Opcodes.ACC_PUBLIC != 0 &&
-                    access and Opcodes.ACC_ANNOTATION != 0 &&
-                    (name.startsWith("android/annotation/") || name.startsWith("androidx/annotation/"))
-                ) {
-                    skip = false
-                    val flagsWithoutPublic = access and Opcodes.ACC_PUBLIC.inv()
-                    super.visit(version, flagsWithoutPublic, name, signature, superName, interfaces)
-                }
-            }
-        }
-
-        reader.accept(classVisitor, 0)
-        return if (skip) {
-            null
-        } else {
-            classWriter.toByteArray()
-        }
-    }
-
-    private fun rewriteJar(file: File) {
-        val temp = File(file.path + ".temp-$PROGRAM_NAME")
-        rewriteJar(file, temp)
-        file.delete()
-        temp.renameTo(file)
-    }
-
-    private val zeroTime = FileTime.fromMillis(0)
-
-    private fun rewriteJar(from: File, to: File/*, filter: Predicate<String>?*/) {
-        Closer.create().use { closer ->
-            val fos = closer.register(FileOutputStream(to))
-            val bos = closer.register(BufferedOutputStream(fos))
-            val zos = closer.register(ZipOutputStream(bos))
-
-            val fis = closer.register(FileInputStream(from))
-            val bis = closer.register(BufferedInputStream(fis))
-            val zis = closer.register(ZipInputStream(bis))
-
-            while (true) {
-                val entry = zis.nextEntry ?: break
-                val name = entry.name
-                val newEntry: JarEntry
-
-                // Preserve the STORED method of the input entry.
-                newEntry = if (entry.method == JarEntry.STORED) {
-                    val jarEntry = JarEntry(entry)
-                    jarEntry.size = entry.size
-                    jarEntry.compressedSize = entry.compressedSize
-                    jarEntry.crc = entry.crc
-                    jarEntry
-                } else {
-                    // Create a new entry so that the compressed len is recomputed.
-                    JarEntry(name)
-                }
-
-                newEntry.lastAccessTime = zeroTime
-                newEntry.creationTime = zeroTime
-                newEntry.lastModifiedTime = entry.lastModifiedTime
-
-                // add the entry to the jar archive
-                zos.putNextEntry(newEntry)
-
-                // read the content of the entry from the input stream, and write it into the archive.
-                if (name.endsWith(DOT_CLASS) &&
-                    (name.startsWith("android/annotation/") || name.startsWith("androidx/annotation/")) &&
-                    name.indexOf("$") == -1 &&
-                    !entry.isDirectory
-                ) {
-                    val bytes = zis.readBytes()
-                    val rewritten = rewriteClass(bytes, name)
-                    if (rewritten != null) {
-                        zos.write(rewritten)
-                    } else {
-                        zos.write(bytes)
-                    }
-                } else {
-                    zis.copyTo(zos)
-                }
-
-                zos.closeEntry()
-                zis.closeEntry()
-            }
-        }
-    }
 }
diff --git a/src/main/java/com/android/tools/metalava/SdkIdentifier.kt b/src/main/java/com/android/tools/metalava/SdkIdentifier.kt
new file mode 100644
index 0000000..7363bbb
--- /dev/null
+++ b/src/main/java/com/android/tools/metalava/SdkIdentifier.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2022 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 com.android.tools.metalava
+
+/**
+ * ID and aliases for a given SDK.
+ *
+ * SDKs include the Android SDK and SDK extensions (e.g. the T extensions).
+ *
+ * @param id: numerical ID of the SDK, primarily used in generated artifacts and consumed by tools
+ * @param shortname: short name for the SDK, primarily used in configuration files
+ * @param name: human readable name for the SDK; used in the official documentation
+ * @param reference: Java symbol in the Android SDK with the same numerical value as the id, using a
+ *                   JVM signature like syntax: "some/clazz$INNER$FIELD"
+ */
+data class SdkIdentifier(val id: Int, val shortname: String, val name: String, val reference: String)
diff --git a/src/main/java/com/android/tools/metalava/SignatureFileLoader.kt b/src/main/java/com/android/tools/metalava/SignatureFileLoader.kt
index 67e75ec..55d434d 100644
--- a/src/main/java/com/android/tools/metalava/SignatureFileLoader.kt
+++ b/src/main/java/com/android/tools/metalava/SignatureFileLoader.kt
@@ -24,40 +24,27 @@
 object SignatureFileLoader {
     private val map = mutableMapOf<File, Codebase>()
 
-    fun load(
-        file: File,
-        kotlinStyleNulls: Boolean? = null
-    ): Codebase {
+    fun load(file: File, kotlinStyleNulls: Boolean = false): Codebase {
         return map[file] ?: run {
-            val loaded = loadFromSignatureFiles(file, kotlinStyleNulls)
+            val loaded = loadFiles(listOf(file), kotlinStyleNulls)
             map[file] = loaded
             loaded
         }
     }
 
-    private fun loadFromSignatureFiles(
-        file: File,
-        kotlinStyleNulls: Boolean? = null
-    ): Codebase {
+    fun loadFiles(files: List<File>, kotlinStyleNulls: Boolean = false): Codebase {
+        require(files.isNotEmpty()) { "files must not be empty" }
+
         try {
-            val codebase = ApiFile.parseApi(File(file.path), kotlinStyleNulls ?: false)
-            codebase.description = "Codebase loaded from ${file.path}"
+            val codebase = ApiFile.parseApi(files, kotlinStyleNulls)
+
+            // Unlike loadFromSources, analyzer methods are not required for text based codebase
+            // because all methods in the API text file belong to an API surface.
+            val analyzer = ApiAnalyzer(codebase)
+            analyzer.addConstructors { _ -> true }
             return codebase
         } catch (ex: ApiParseException) {
-            val message = "Unable to parse signature file $file: ${ex.message}"
-            throw DriverException(message)
-        }
-    }
-
-    fun loadFiles(files: List<File>, kotlinStyleNulls: Boolean? = null): Codebase {
-        if (files.isEmpty()) {
-            throw IllegalArgumentException("files must not be empty")
-        }
-        try {
-            return ApiFile.parseApi(files, kotlinStyleNulls ?: false)
-        } catch (ex: ApiParseException) {
-            val message = "Unable to parse signature file: ${ex.message}"
-            throw DriverException(message)
+            throw DriverException("Unable to parse signature file: ${ex.message}")
         }
     }
 }
diff --git a/src/main/java/com/android/tools/metalava/SignatureWriter.kt b/src/main/java/com/android/tools/metalava/SignatureWriter.kt
index 0a04a5f..0310322 100644
--- a/src/main/java/com/android/tools/metalava/SignatureWriter.kt
+++ b/src/main/java/com/android/tools/metalava/SignatureWriter.kt
@@ -77,8 +77,7 @@
     override fun visitConstructor(constructor: ConstructorItem) {
         write("    ctor ")
         writeModifiers(constructor)
-        // Note - we don't write out the type parameter list (constructor.typeParameterList()) in signature files!
-        // writeTypeParameterList(constructor.typeParameterList(), addSpace = true)
+        writeTypeParameterList(constructor.typeParameterList(), addSpace = true)
         write(constructor.containingClass().fullName())
         writeParameterList(constructor)
         writeThrowsList(constructor)
diff --git a/src/main/java/com/android/tools/metalava/apilevels/AndroidJarReader.java b/src/main/java/com/android/tools/metalava/apilevels/AndroidJarReader.java
deleted file mode 100644
index 67ba3b6..0000000
--- a/src/main/java/com/android/tools/metalava/apilevels/AndroidJarReader.java
+++ /dev/null
@@ -1,196 +0,0 @@
-/*
- * Copyright (C) 2017 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 com.android.tools.metalava.apilevels;
-
-import com.android.SdkConstants;
-import com.android.tools.metalava.model.Codebase;
-import com.google.common.io.ByteStreams;
-import com.google.common.io.Closeables;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-import org.objectweb.asm.ClassReader;
-import org.objectweb.asm.Opcodes;
-import org.objectweb.asm.tree.ClassNode;
-import org.objectweb.asm.tree.FieldNode;
-import org.objectweb.asm.tree.MethodNode;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.util.List;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipInputStream;
-
-/**
- * Reads all the android.jar files found in an SDK and generate a map of {@link ApiClass}.
- */
-class AndroidJarReader {
-    private int mMinApi;
-    private int mCurrentApi;
-    private File mCurrentJar;
-    private List<String> mPatterns;
-    private File[] mApiLevels;
-    private final Codebase mCodebase;
-
-    AndroidJarReader(@NotNull List<String> patterns,
-                     int minApi,
-                     @NotNull File currentJar,
-                     int currentApi,
-                     @Nullable Codebase codebase) {
-        mPatterns = patterns;
-        mMinApi = minApi;
-        mCurrentJar = currentJar;
-        mCurrentApi = currentApi;
-        mCodebase = codebase;
-    }
-
-    AndroidJarReader(@NotNull File[] apiLevels, int firstApiLevel, @Nullable Codebase codebase) {
-        mApiLevels = apiLevels;
-        mCodebase = codebase;
-        mMinApi = firstApiLevel;
-    }
-
-    public Api getApi() throws IOException {
-        Api api;
-        if (mApiLevels != null) {
-            int max = mApiLevels.length - 1;
-            if (mCodebase != null) {
-                max = mCodebase.getApiLevel();
-            }
-
-            api = new Api(mMinApi, max);
-            for (int apiLevel = mMinApi; apiLevel < mApiLevels.length; apiLevel++) {
-                File jar = getAndroidJarFile(apiLevel);
-                readJar(api, apiLevel, jar);
-            }
-            if (mCodebase != null) {
-                int apiLevel = mCodebase.getApiLevel();
-                if (apiLevel != -1) {
-                    processCodebase(api, apiLevel);
-                }
-            }
-        } else {
-            api = new Api(mMinApi, mCurrentApi);
-            // Get all the android.jar. They are in platforms-#
-            int apiLevel = mMinApi - 1;
-            while (true) {
-                apiLevel++;
-                File jar = null;
-                if (apiLevel == mCurrentApi) {
-                    jar = mCurrentJar;
-                }
-                if (jar == null) {
-                    jar = getAndroidJarFile(apiLevel);
-                }
-                if (jar == null || !jar.isFile()) {
-                    if (mCodebase != null) {
-                        processCodebase(api, apiLevel);
-                    }
-                    break;
-                }
-                readJar(api, apiLevel, jar);
-            }
-        }
-
-        api.inlineFromHiddenSuperClasses();
-        api.removeImplicitInterfaces();
-        api.removeOverridingMethods();
-        api.prunePackagePrivateClasses();
-
-        return api;
-    }
-
-    private void processCodebase(Api api, int apiLevel) {
-        if (mCodebase == null) {
-            return;
-        }
-        AddApisFromCodebaseKt.addApisFromCodebase(api, apiLevel, mCodebase);
-    }
-
-    private void readJar(Api api, int apiLevel, File jar) throws IOException {
-        api.update(apiLevel);
-
-        FileInputStream fis = new FileInputStream(jar);
-        ZipInputStream zis = new ZipInputStream(fis);
-        ZipEntry entry = zis.getNextEntry();
-        while (entry != null) {
-            String name = entry.getName();
-
-            if (name.endsWith(SdkConstants.DOT_CLASS)) {
-                byte[] bytes = ByteStreams.toByteArray(zis);
-                ClassReader reader = new ClassReader(bytes);
-                ClassNode classNode = new ClassNode(Opcodes.ASM5);
-                reader.accept(classNode, 0 /*flags*/);
-
-                ApiClass theClass = api.addClass(classNode.name, apiLevel,
-                    (classNode.access & Opcodes.ACC_DEPRECATED) != 0);
-
-                theClass.updateHidden(apiLevel, (classNode.access & Opcodes.ACC_PUBLIC) == 0);
-
-                // super class
-                if (classNode.superName != null) {
-                    theClass.addSuperClass(classNode.superName, apiLevel);
-                }
-
-                // interfaces
-                for (Object interfaceName : classNode.interfaces) {
-                    theClass.addInterface((String) interfaceName, apiLevel);
-                }
-
-                // fields
-                for (Object field : classNode.fields) {
-                    FieldNode fieldNode = (FieldNode) field;
-                    if (((fieldNode.access & (Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED)) == 0)) {
-                        continue;
-                    }
-                    if (!fieldNode.name.startsWith("this$") &&
-                        !fieldNode.name.equals("$VALUES")) {
-                        boolean deprecated = (fieldNode.access & Opcodes.ACC_DEPRECATED) != 0;
-                        theClass.addField(fieldNode.name, apiLevel, deprecated);
-                    }
-                }
-
-                // methods
-                for (Object method : classNode.methods) {
-                    MethodNode methodNode = (MethodNode) method;
-                    if (((methodNode.access & (Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED)) == 0)) {
-                        continue;
-                    }
-                    if (!methodNode.name.equals("<clinit>")) {
-                        boolean deprecated = (methodNode.access & Opcodes.ACC_DEPRECATED) != 0;
-                        theClass.addMethod(methodNode.name + methodNode.desc, apiLevel, deprecated);
-                    }
-                }
-            }
-            entry = zis.getNextEntry();
-        }
-
-        Closeables.close(fis, true);
-    }
-
-    private File getAndroidJarFile(int apiLevel) {
-        if (mApiLevels != null) {
-            return mApiLevels[apiLevel];
-        }
-        for (String pattern : mPatterns) {
-            File f = new File(pattern.replace("%", Integer.toString(apiLevel)));
-            if (f.isFile()) {
-                return f;
-            }
-        }
-        return null;
-    }
-}
diff --git a/src/main/java/com/android/tools/metalava/apilevels/Api.java b/src/main/java/com/android/tools/metalava/apilevels/Api.java
index de08cf5..a79af6a 100644
--- a/src/main/java/com/android/tools/metalava/apilevels/Api.java
+++ b/src/main/java/com/android/tools/metalava/apilevels/Api.java
@@ -15,10 +15,16 @@
  */
 package com.android.tools.metalava.apilevels;
 
+import com.android.tools.metalava.SdkIdentifier;
+
 import java.io.PrintStream;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
 
 /**
  * Represents the whole Android API.
@@ -26,12 +32,10 @@
 public class Api extends ApiElement {
     private final Map<String, ApiClass> mClasses = new HashMap<>();
     private final int mMin;
-    private final int mMax;
 
-    public Api(int min, int max) {
+    public Api(int min) {
         super("Android API");
         mMin = min;
-        mMax = max;
     }
 
     /**
@@ -39,12 +43,16 @@
      *
      * @param stream the stream to print the XML elements to
      */
-    public void print(PrintStream stream) {
-        stream.print("<api version=\"2\"");
+    public void print(PrintStream stream, Set<SdkIdentifier> sdkIdentifiers) {
+        stream.print("<api version=\"3\"");
         if (mMin > 1) {
             stream.print(" min=\"" + mMin + "\"");
         }
         stream.println(">");
+        for (SdkIdentifier sdk : sdkIdentifiers) {
+            stream.println(String.format("\t<sdk id=\"%d\" shortname=\"%s\" name=\"%s\" reference=\"%s\"/>",
+                        sdk.getId(), sdk.getShortname(), sdk.getName(), sdk.getReference()));
+        }
         print(mClasses.values(), "class", "\t", stream);
         printClosingTag("api", "", stream);
     }
@@ -75,6 +83,37 @@
         return mClasses.get(name);
     }
 
+    public Collection<ApiClass> getClasses() {
+        return Collections.unmodifiableCollection(mClasses.values());
+    }
+
+    public void backfillHistoricalFixes() {
+        backfillSdkExtensions();
+    }
+
+    private void backfillSdkExtensions() {
+        // SdkExtensions.getExtensionVersion was added in 30/R, but was a SystemApi
+        // to avoid publishing the versioning API publicly before there was any
+        // valid use for it.
+        // getAllExtensionsVersions was added as part of 31/S
+        // The class and its APIs were made public between S and T, but we pretend
+        // here like it was always public, for maximum backward compatibility.
+        ApiClass sdkExtensions = findClass("android/os/ext/SdkExtensions");
+
+        if (sdkExtensions != null && sdkExtensions.getSince() != 30
+                && sdkExtensions.getSince() != 33) {
+            throw new AssertionError("Received unexpected historical data");
+        } else if (sdkExtensions == null || sdkExtensions.getSince() == 30) {
+            // This is the system API db (30), or module-lib/system-server dbs (null)
+            // They don't need patching.
+            return;
+        }
+        sdkExtensions.update(30, false);
+        sdkExtensions.addSuperClass("java/lang/Object", 30);
+        sdkExtensions.getMethod("getExtensionVersion(I)I").update(30, false);
+        sdkExtensions.getMethod("getAllExtensionVersions()Ljava/util/Map;").update(31, false);
+    }
+
     /**
      * The bytecode visitor registers interfaces listed for a class. However,
      * a class will <b>also</b> implement interfaces implemented by the super classes.
@@ -115,4 +154,40 @@
             cls.removeHiddenSuperClasses(mClasses);
         }
     }
+
+    public void removeMissingClasses() {
+        for (ApiClass cls : mClasses.values()) {
+            cls.removeMissingClasses(mClasses);
+        }
+    }
+
+    public void verifyNoMissingClasses() {
+        Map<String, Set<String>> results = new TreeMap<>();
+        for (ApiClass cls : mClasses.values()) {
+            Set<ApiElement> missing = cls.findMissingClasses(mClasses);
+            // Have the missing classes as keys, and the referencing classes as values.
+            for (ApiElement missingClass : missing) {
+                String missingName = missingClass.getName();
+                if (!results.containsKey(missingName)) {
+                    results.put(missingName, new TreeSet<>());
+                }
+                results.get(missingName).add(cls.getName());
+            }
+        }
+        if (!results.isEmpty()) {
+            String message = "";
+            for (Map.Entry<String, Set<String>> entry : results.entrySet()) {
+                message += "\n  " + entry.getKey() + " referenced by:";
+                for (String referencer : entry.getValue()) {
+                    message += "\n    " + referencer;
+                }
+            }
+            throw new IllegalStateException("There are classes in this API that reference other "+
+                "classes that do not exist in this API. "+
+                "This can happen when an api is provided by an apex, but referenced "+
+                "from non-updatable platform code. Use --remove-missing-classes-in-api-levels to "+
+                "make metalava remove these references instead of erroring out."+
+                message);
+        }
+    }
 }
diff --git a/src/main/java/com/android/tools/metalava/apilevels/ApiClass.java b/src/main/java/com/android/tools/metalava/apilevels/ApiClass.java
index a40cd72..1184358 100644
--- a/src/main/java/com/android/tools/metalava/apilevels/ApiClass.java
+++ b/src/main/java/com/android/tools/metalava/apilevels/ApiClass.java
@@ -21,11 +21,15 @@
 import java.io.PrintStream;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.ListIterator;
 import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 
 /**
  * Represents a class or an interface and its methods/fields.
@@ -43,23 +47,23 @@
      */
     private int mPrivateUntil; // Package private class?
 
-    private final Map<String, ApiElement> mFields = new HashMap<>();
-    private final Map<String, ApiElement> mMethods = new HashMap<>();
+    private final Map<String, ApiElement> mFields = new ConcurrentHashMap<>();
+    private final Map<String, ApiElement> mMethods = new ConcurrentHashMap<>();
 
     public ApiClass(String name, int version, boolean deprecated) {
         super(name, version, deprecated);
     }
 
-    public void addField(String name, int version, boolean deprecated) {
-        addToMap(mFields, name, version, deprecated);
+    public ApiElement addField(String name, int version, boolean deprecated) {
+        return addToMap(mFields, name, version, deprecated);
     }
 
-    public void addMethod(String name, int version, boolean deprecated) {
+    public ApiElement addMethod(String name, int version, boolean deprecated) {
         // Correct historical mistake in android.jar files
         if (name.endsWith(")Ljava/lang/AbstractStringBuilder;")) {
             name = name.substring(0, name.length() - ")Ljava/lang/AbstractStringBuilder;".length()) + ")L" + getName() + ";";
         }
-        addToMap(mMethods, name, version, deprecated);
+        return addToMap(mMethods, name, version, deprecated);
     }
 
     public ApiElement addSuperClass(String superClass, int since) {
@@ -107,7 +111,7 @@
         return mInterfaces;
     }
 
-    private void addToMap(Map<String, ApiElement> elements, String name, int version, boolean deprecated) {
+    private ApiElement addToMap(Map<String, ApiElement> elements, String name, int version, boolean deprecated) {
         ApiElement element = elements.get(name);
         if (element == null) {
             element = new ApiElement(name, version, deprecated);
@@ -115,6 +119,7 @@
         } else {
             element.update(version, deprecated);
         }
+        return element;
     }
 
     private ApiElement addToArray(Collection<ApiElement> elements, String name, int version) {
@@ -311,4 +316,62 @@
             }
         }
     }
+
+    // Ensure this class doesn't extend/implement any other classes/interfaces that are
+    // not in the provided api. This can happen when a class in an android.jar file
+    // encodes the inheritance, but the class that is inherited is not present in any
+    // android.jar file. The class would instead be present in an apex's stub jar file.
+    // An example of this is the QosSessionAttributes interface being provided by the
+    // Connectivity apex, but being implemented by NrQosSessionAttributes from
+    // frameworks/base/telephony.
+    public void removeMissingClasses(Map<String, ApiClass> api) {
+        Iterator<ApiElement> superClassIter = mSuperClasses.iterator();
+        while (superClassIter.hasNext()) {
+            ApiElement scls = superClassIter.next();
+            if (!api.containsKey(scls.getName())) {
+                superClassIter.remove();
+            }
+        }
+
+        Iterator<ApiElement> interfacesIter = mInterfaces.iterator();
+        while (interfacesIter.hasNext()) {
+            ApiElement intf = interfacesIter.next();
+            if (!api.containsKey(intf.getName())) {
+                interfacesIter.remove();
+            }
+        }
+    }
+
+    // Returns the set of superclasses or interfaces are not present in the provided api map
+    public Set<ApiElement> findMissingClasses(Map<String, ApiClass> api) {
+        Set<ApiElement> result = new HashSet<>();
+        for (ApiElement scls : mSuperClasses) {
+            if (!api.containsKey(scls.getName())) {
+                result.add(scls);
+            }
+        }
+
+        for (ApiElement intf : mInterfaces) {
+            if (!api.containsKey(intf.getName())) {
+                result.add(intf);
+            }
+        }
+        return result;
+    }
+
+    public Iterator<ApiElement> getFieldIterator() {
+        return mFields.values().iterator();
+    }
+
+    public Iterator<ApiElement> getMethodIterator() {
+        return mMethods.values().iterator();
+    }
+
+    public ApiElement getField(String name) {
+        return mFields.get(name);
+    }
+
+    public ApiElement getMethod(String name) {
+        return mMethods.get(name);
+    }
 }
diff --git a/src/main/java/com/android/tools/metalava/apilevels/ApiElement.java b/src/main/java/com/android/tools/metalava/apilevels/ApiElement.java
index ec58ffc..eee7d9b 100644
--- a/src/main/java/com/android/tools/metalava/apilevels/ApiElement.java
+++ b/src/main/java/com/android/tools/metalava/apilevels/ApiElement.java
@@ -22,24 +22,49 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 
 /**
  * Represents an API element, e.g. class, method or field.
  */
 public class ApiElement implements Comparable<ApiElement> {
+    public static final int NEVER = Integer.MAX_VALUE;
+
     private final String mName;
+
+    /**
+     * The Android platform SDK version this API was first introduced in.
+     */
     private int mSince;
+
+    /**
+     * The Android extension SDK version this API was first introduced in.
+     */
+    private int mSinceExtension = NEVER;
+
+    /**
+     * The SDKs and their versions this API was first introduced in.
+     *
+     * The value is a comma-separated list of &lt;int&gt;:&lt;int&gt; values, where the first
+     * &lt;int&gt; is the integer ID of an SDK, and the second &lt;int&gt; the version of that SDK,
+     * in which this API first appeared.
+     *
+     * This field is a super-set of mSince, and if non-null/non-empty, should be preferred.
+     */
+    private String mSdks;
+
+    private String mMainlineModule;
     private int mDeprecatedIn;
     private int mLastPresentIn;
 
     /**
      * @param name       the name of the API element
-     * @param version    an API version for which the API element existed
+     * @param version    an API version for which the API element existed, or -1 if the class does
+     *                   not yet exist in the Android SDK (only in extension SDKs)
      * @param deprecated whether the API element was deprecated in the API version in question
      */
     ApiElement(String name, int version, boolean deprecated) {
         assert name != null;
-        assert version > 0;
         mName = name;
         mSince = version;
         mLastPresentIn = version;
@@ -68,11 +93,21 @@
         return mName;
     }
 
+    /**
+     * The Android API level of this ApiElement.
+     */
     public int getSince() {
         return mSince;
     }
 
     /**
+     * The extension version of this ApiElement.
+     */
+    public int getSinceExtension() {
+        return mSinceExtension;
+    }
+
+    /**
      * Checks if this API element was introduced not later than another API element.
      *
      * @param other the API element to compare to
@@ -113,6 +148,24 @@
     }
 
     /**
+     * Analoguous to update(), but for extensions sdk versions.
+     *
+     * @param version an extension SDK version for which the API element existed
+     */
+    public void updateExtension(int version) {
+        assert version > 0;
+        if (mSinceExtension > version) {
+            mSinceExtension = version;
+        }
+    }
+
+    public void updateSdks(String sdks) { mSdks = sdks; }
+
+    public void updateMainlineModule(String module) { mMainlineModule = module; }
+
+    public String getMainlineModule() { return mMainlineModule; }
+
+    /**
      * Checks whether the API element is deprecated or not.
      */
     public final boolean isDeprecated() {
@@ -151,10 +204,18 @@
         stream.print(tag);
         stream.print(" name=\"");
         stream.print(encodeAttribute(mName));
+        if (!isEmpty(mMainlineModule) && !isEmpty(mSdks)) {
+            stream.print("\" module=\"");
+            stream.print(encodeAttribute(mMainlineModule));
+        }
         if (mSince > parentElement.mSince) {
             stream.print("\" since=\"");
             stream.print(mSince);
         }
+        if (!isEmpty(mSdks) && !Objects.equals(mSdks, parentElement.mSdks)) {
+            stream.print("\" sdks=\"");
+            stream.print(mSdks);
+        }
         if (mDeprecatedIn != 0) {
             stream.print("\" deprecated=\"");
             stream.print(mDeprecatedIn);
@@ -170,6 +231,10 @@
         stream.println('>');
     }
 
+    private boolean isEmpty(String s) {
+        return s == null || s.isEmpty();
+    }
+
     /**
      * Prints homogeneous XML elements to a stream. Each element is printed on a separate line.
      * Attributes with values matching the parent API element are omitted.
diff --git a/src/main/java/com/android/tools/metalava/apilevels/ApiGenerator.java b/src/main/java/com/android/tools/metalava/apilevels/ApiGenerator.java
index 28dec48..bfbdf9d 100644
--- a/src/main/java/com/android/tools/metalava/apilevels/ApiGenerator.java
+++ b/src/main/java/com/android/tools/metalava/apilevels/ApiGenerator.java
@@ -17,177 +17,151 @@
 package com.android.tools.metalava.apilevels;
 
 import com.android.tools.metalava.model.Codebase;
+import com.android.tools.metalava.SdkIdentifier;
+
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 import java.io.File;
 import java.io.IOException;
 import java.io.PrintStream;
+import java.nio.file.Files;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
 
 /**
  * Main class for command line command to convert the existing API XML/TXT files into diff-based
  * simple text files.
  */
 public class ApiGenerator {
-    public static void main(String[] args) {
-        boolean error = false;
-        int minApi = 1;
-        int currentApi = -1;
-        String currentCodename = null;
-        File currentJar = null;
-        List<String> patterns = new ArrayList<>();
-        String outPath = null;
-
-        for (int i = 0; i < args.length && !error; i++) {
-            String arg = args[i];
-
-            if (arg.equals("--pattern")) {
-                i++;
-                if (i < args.length) {
-                    patterns.add(args[i]);
-                } else {
-                    System.err.println("Missing argument after " + arg);
-                    error = true;
-                }
-            } else if (arg.equals("--current-version")) {
-                i++;
-                if (i < args.length) {
-                    currentApi = Integer.parseInt(args[i]);
-                    if (currentApi <= 22) {
-                        System.err.println("Suspicious currentApi=" + currentApi + ", expected at least 23");
-                        error = true;
-                    }
-                } else {
-                    System.err.println("Missing number >= 1 after " + arg);
-                    error = true;
-                }
-            } else if (arg.equals("--current-codename")) {
-                i++;
-                if (i < args.length) {
-                    currentCodename = args[i];
-                } else {
-                    System.err.println("Missing codename after " + arg);
-                    error = true;
-                }
-            } else if (arg.equals("--current-jar")) {
-                i++;
-                if (i < args.length) {
-                    if (currentJar != null) {
-                        System.err.println("--current-jar should only be specified once");
-                        error = true;
-                    }
-                    String path = args[i];
-                    currentJar = new File(path);
-                } else {
-                    System.err.println("Missing argument after " + arg);
-                    error = true;
-                }
-            } else if (arg.equals("--min-api")) {
-                i++;
-                if (i < args.length) {
-                    minApi = Integer.parseInt(args[i]);
-                } else {
-                    System.err.println("Missing number >= 1 after " + arg);
-                    error = true;
-                }
-            } else if (arg.length() >= 2 && arg.startsWith("--")) {
-                System.err.println("Unknown argument: " + arg);
-                error = true;
-            } else if (outPath == null) {
-                outPath = arg;
-            } else if (new File(arg).isDirectory()) {
-                String pattern = arg;
-                if (!pattern.endsWith(File.separator)) {
-                    pattern += File.separator;
-                }
-                pattern += "platforms" + File.separator + "android-%" + File.separator + "android.jar";
-                patterns.add(pattern);
-            } else {
-                System.err.println("Unknown argument: " + arg);
-                error = true;
-            }
-        }
-
-        if (!error && outPath == null) {
-            System.err.println("Missing out file path");
-            error = true;
-        }
-
-        if (!error && patterns.isEmpty()) {
-            System.err.println("Missing SdkFolder or --pattern.");
-            error = true;
-        }
-
-        if (currentJar != null && currentApi == -1 || currentJar == null && currentApi != -1) {
-            System.err.println("You must specify both --current-jar and --current-version (or neither one)");
-            error = true;
-        }
-
-        // The SDK version number
-        if (currentCodename != null && !"REL".equals(currentCodename)) {
-            currentApi++;
-        }
-
-        if (error) {
-            printUsage();
-            System.exit(1);
-        }
-
-        try {
-            if (!generate(minApi, currentApi, currentJar, patterns, outPath, null)) {
-                System.exit(1);
-            }
-        } catch (IOException e) {
-            e.printStackTrace();
-            System.exit(-1);
-        }
-    }
-
-    private static boolean generate(int minApi,
-                                    int currentApi,
-                                    @NotNull File currentJar,
-                                    @NotNull List<String> patterns,
-                                    @NotNull String outPath,
-                                    @Nullable Codebase codebase) throws IOException {
-        AndroidJarReader reader = new AndroidJarReader(patterns, minApi, currentJar, currentApi, codebase);
-        Api api = reader.getApi();
-        return createApiFile(new File(outPath), api);
-    }
-
     public static boolean generate(@NotNull File[] apiLevels,
                                    int firstApiLevel,
+                                   int currentApiLevel,
+                                   boolean isDeveloperPreviewBuild,
                                    @NotNull File outputFile,
-                                   @Nullable Codebase codebase) throws IOException {
-        AndroidJarReader reader = new AndroidJarReader(apiLevels, firstApiLevel, codebase);
-        Api api = reader.getApi();
-        return createApiFile(outputFile, api);
+                                   @NotNull Codebase codebase,
+                                   @Nullable File sdkJarRoot,
+                                   @Nullable File sdkFilterFile,
+                                   boolean removeMissingClasses) throws IOException, IllegalArgumentException {
+        if ((sdkJarRoot == null) != (sdkFilterFile == null)) {
+            throw new IllegalArgumentException("sdkJarRoot and sdkFilterFile must both be null, or non-null");
+        }
+
+        int notFinalizedApiLevel = currentApiLevel + 1;
+        Api api = readAndroidJars(apiLevels, firstApiLevel);
+        if (isDeveloperPreviewBuild || apiLevels.length - 1 < currentApiLevel) {
+            // Only include codebase if we don't have a prebuilt, finalized jar for it.
+            int apiLevel = isDeveloperPreviewBuild ? notFinalizedApiLevel : currentApiLevel;
+            AddApisFromCodebaseKt.addApisFromCodebase(api, apiLevel, codebase);
+        }
+        api.backfillHistoricalFixes();
+
+        Set<SdkIdentifier> sdkIdentifiers = Collections.emptySet();
+        if (sdkJarRoot != null && sdkFilterFile != null) {
+            sdkIdentifiers = processExtensionSdkApis(api, notFinalizedApiLevel, sdkJarRoot, sdkFilterFile);
+        }
+        api.inlineFromHiddenSuperClasses();
+        api.removeImplicitInterfaces();
+        api.removeOverridingMethods();
+        api.prunePackagePrivateClasses();
+        if (removeMissingClasses) {
+            api.removeMissingClasses();
+        } else {
+            api.verifyNoMissingClasses();
+        }
+        return createApiFile(outputFile, api, sdkIdentifiers);
     }
 
-    private static void printUsage() {
-        System.err.println("\nGenerates a single API file from the content of an SDK.");
-        System.err.println("Usage:");
-        System.err.println("\tApiCheck [--min-api=1] OutFile [SdkFolder | --pattern sdk/%/public/android.jar]+");
-        System.err.println("Options:");
-        System.err.println("--min-api <int> : The first API level to consider (>=1).");
-        System.err.println("--pattern <pattern>: Path pattern to find per-API android.jar files, where\n" +
-            "            '%' is replaced by the API level.");
-        System.err.println("--current-jar <path>: Path pattern to find the current android.jar");
-        System.err.println("--current-version <int>: The API level for the current API");
-        System.err.println("--current-codename <name>: REL, if a release, or codename for previews");
-        System.err.println("SdkFolder: if given, this adds the pattern\n" +
-            "           '$SdkFolder/platforms/android-%/android.jar'");
-        System.err.println("If multiple --pattern are specified, they are tried in the order given.\n");
+    private static Api readAndroidJars(File[] apiLevels, int firstApiLevel) {
+        Api api = new Api(firstApiLevel);
+        for (int apiLevel = firstApiLevel; apiLevel < apiLevels.length; apiLevel++) {
+            File jar = apiLevels[apiLevel];
+            JarReaderUtilsKt.readAndroidJar(api, apiLevel, jar);
+        }
+        return api;
+    }
+
+    /**
+     * Modify the extension SDK API parts of an API as dictated by a filter.
+     *
+     *   - remove APIs not listed in the filter
+     *   - assign APIs listed in the filter their corresponding extensions
+     *
+     * Some APIs only exist in extension SDKs and not in the Android SDK, but for backwards
+     * compatibility with tools that expect the Android SDK to be the only SDK, metalava needs to
+     * assign such APIs some Android SDK API level. The recommended value is current-api-level + 1,
+     * which is what non-finalized APIs use.
+     *
+     * @param api the api to modify
+     * @param apiLevelNotInAndroidSdk fallback API level for APIs not in the Android SDK
+     * @param sdkJarRoot path to directory containing extension SDK jars (usually $ANDROID_ROOT/prebuilts/sdk/extensions)
+     * @param filterPath: path to the filter file. @see ApiToExtensionsMap
+     * @throws IOException if the filter file can not be read
+     * @throws IllegalArgumentException if an error is detected in the filter file, or if no jar files were found
+     */
+    private static Set<SdkIdentifier> processExtensionSdkApis(
+            @NotNull Api api,
+            int apiLevelNotInAndroidSdk,
+            @NotNull File sdkJarRoot,
+            @NotNull File filterPath) throws IOException, IllegalArgumentException {
+        String rules = new String(Files.readAllBytes(filterPath.toPath()));
+
+        Map<String, List<VersionAndPath>> map = ExtensionSdkJarReader.Companion.findExtensionSdkJarFiles(sdkJarRoot);
+        if (map.isEmpty()) {
+            throw new IllegalArgumentException("no extension sdk jar files found in " + sdkJarRoot);
+        }
+        Map<String, ApiToExtensionsMap> moduleMaps = new HashMap<>();
+        for (Map.Entry<String, List<VersionAndPath>> entry : map.entrySet()) {
+            String mainlineModule = entry.getKey();
+            ApiToExtensionsMap moduleMap = ApiToExtensionsMap.Companion.fromXml(mainlineModule, rules);
+            if (moduleMap.isEmpty()) continue; // TODO(b/259115852): remove this (though it is an optimization too).
+
+            moduleMaps.put(mainlineModule, moduleMap);
+            for (VersionAndPath f : entry.getValue()) {
+                JarReaderUtilsKt.readExtensionJar(api, f.version, mainlineModule, f.path, apiLevelNotInAndroidSdk);
+            }
+        }
+        for (ApiClass clazz : api.getClasses()) {
+            String module = clazz.getMainlineModule();
+            if (module == null) continue;
+            ApiToExtensionsMap extensionsMap = moduleMaps.get(module);
+            String sdks = extensionsMap.calculateSdksAttr(clazz.getSince(), apiLevelNotInAndroidSdk,
+                extensionsMap.getExtensions(clazz), clazz.getSinceExtension());
+            clazz.updateSdks(sdks);
+
+            Iterator<ApiElement> iter = clazz.getFieldIterator();
+            while (iter.hasNext()) {
+                ApiElement field = iter.next();
+                sdks = extensionsMap.calculateSdksAttr(field.getSince(), apiLevelNotInAndroidSdk,
+                    extensionsMap.getExtensions(clazz, field), field.getSinceExtension());
+                field.updateSdks(sdks);
+            }
+
+            iter = clazz.getMethodIterator();
+            while (iter.hasNext()) {
+                ApiElement method = iter.next();
+                sdks = extensionsMap.calculateSdksAttr(method.getSince(), apiLevelNotInAndroidSdk,
+                    extensionsMap.getExtensions(clazz, method), method.getSinceExtension());
+                method.updateSdks(sdks);
+            }
+        }
+        return ApiToExtensionsMap.Companion.fromXml("", rules).getSdkIdentifiers();
     }
 
     /**
      * Creates the simplified diff-based API level.
      *
-     * @param outFile the output file
-     * @param api     the api to write
+     * @param outFile        the output file
+     * @param api            the api to write
+     * @param sdkIdentifiers SDKs referenced by the api
      */
-    private static boolean createApiFile(File outFile, Api api) {
+    private static boolean createApiFile(@NotNull File outFile, @NotNull Api api, @NotNull Set<SdkIdentifier> sdkIdentifiers) {
         File parentFile = outFile.getParentFile();
         if (!parentFile.exists()) {
             boolean ok = parentFile.mkdirs();
@@ -198,7 +172,7 @@
         }
         try (PrintStream stream = new PrintStream(outFile, "UTF-8")) {
             stream.println("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
-            api.print(stream);
+            api.print(stream, sdkIdentifiers);
         } catch (Exception e) {
             e.printStackTrace();
             return false;
diff --git a/src/main/java/com/android/tools/metalava/apilevels/ApiToExtensionsMap.kt b/src/main/java/com/android/tools/metalava/apilevels/ApiToExtensionsMap.kt
new file mode 100644
index 0000000..087d690
--- /dev/null
+++ b/src/main/java/com/android/tools/metalava/apilevels/ApiToExtensionsMap.kt
@@ -0,0 +1,309 @@
+/*
+ * Copyright (C) 2022 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 com.android.tools.metalava.apilevels
+
+import com.android.tools.metalava.SdkIdentifier
+import org.xml.sax.Attributes
+import org.xml.sax.helpers.DefaultHandler
+import javax.xml.parsers.SAXParserFactory
+
+/**
+ * A filter of classes, fields and methods that are allowed in and extension SDK, and for each item,
+ * what extension SDK it first appeared in. Also, a mapping between SDK name and numerical ID.
+ *
+ * Internally, the filers are represented as a tree, where each node in the tree matches a part of a
+ * package, class or member name. For example, given the patterns
+ *
+ *   com.example.Foo             -> [A]
+ *   com.example.Foo#someMethod  -> [B]
+ *   com.example.Bar             -> [A, C]
+ *
+ * (anything prefixed with com.example.Foo is allowed and part of the A extension, except for
+ * com.example.Foo#someMethod which is part of B; anything prefixed with com.example.Bar is part
+ * of both A and C), the internal tree looks like
+ *
+ *   root -> null
+ *     com -> null
+ *       example -> null
+ *         Foo -> [A]
+ *           someMethod -> [B]
+ *         Bar -> [A, C]
+ */
+class ApiToExtensionsMap private constructor(
+    private val sdkIdentifiers: Set<SdkIdentifier>,
+    private val root: Node,
+) {
+    fun isEmpty(): Boolean = root.children.isEmpty() && root.extensions.isEmpty()
+
+    fun getExtensions(clazz: ApiClass): List<String> = getExtensions(clazz.name.toDotNotation())
+
+    fun getExtensions(clazz: ApiClass, member: ApiElement): List<String> =
+        getExtensions(clazz.name.toDotNotation() + "#" + member.name.toDotNotation())
+
+    fun getExtensions(what: String): List<String> {
+        // Special case: getExtensionVersion is not part of an extension
+        val sdkExtensions = "android.os.ext.SdkExtensions"
+        if (what == sdkExtensions || what == "$sdkExtensions#getExtensionVersion") {
+            return listOf()
+        }
+
+        val parts = what.split(REGEX_DELIMITERS)
+
+        var lastSeenExtensions = root.extensions
+        var node = root.children.findNode(parts[0]) ?: return lastSeenExtensions
+        if (node.extensions.isNotEmpty()) {
+            lastSeenExtensions = node.extensions
+        }
+
+        for (part in parts.stream().skip(1)) {
+            node = node.children.findNode(part) ?: break
+            if (node.extensions.isNotEmpty()) {
+                lastSeenExtensions = node.extensions
+            }
+        }
+        return lastSeenExtensions
+    }
+
+    fun getSdkIdentifiers(): Set<SdkIdentifier> = sdkIdentifiers.toSet()
+
+    /**
+     * Construct a `sdks` attribute value
+     *
+     * `sdks` is an XML attribute on class, method and fields in the XML generated by ARG_GENERATE_API_LEVELS.
+     * It expresses in what SDKs an API exist, and in which version of each SDK it was first introduced;
+     * `sdks` replaces the `since` attribute.
+     *
+     * The format of `sdks` is
+     *
+     * sdks="ext:version[,ext:version[,...]]
+     *
+     * where <ext> is the numerical ID of the SDK, and <version> is the version in which the API was introduced.
+     *
+     * The returned string is guaranteed to be one of
+     *
+     * - list of (extensions,finalized_version) pairs + ANDROID_SDK:finalized_dessert
+     * - list of (extensions,finalized_version) pairs
+     * - ANDROID_SDK:finalized_dessert
+     * - ANDROID_SDK:next_dessert_int (for symbols not finalized anywhere)
+     *
+     * See go/mainline-sdk-api-versions-xml for more information.
+     *
+     * @param androidSince Android dessert version in which this symbol was finalized, or notFinalizedValue
+     *                     if this symbol has not been finalized in an Android dessert
+     * @param notFinalizedValue value used together with the Android SDK ID to indicate that this symbol
+     *                          has not been finalized at all
+     * @param extensions names of the SDK extensions in which this symbol has been finalized (may be non-empty
+     *                   even if extensionsSince is ApiElement.NEVER)
+     * @param extensionsSince the version of the SDK extensions in which this API was initially introduced
+     *                        (same value for all SDK extensions), or ApiElement.NEVER if this symbol
+     *                        has not been finalized in any SDK extension (regardless of the extensions argument)
+     * @return an `sdks` value suitable for including verbatim in XML
+     */
+    fun calculateSdksAttr(
+        androidSince: Int,
+        notFinalizedValue: Int,
+        extensions: List<String>,
+        extensionsSince: Int
+    ): String {
+        // Special case: symbol not finalized anywhere -> "ANDROID_SDK:next_dessert_int"
+        if (androidSince == notFinalizedValue && extensionsSince == ApiElement.NEVER) {
+            return "$ANDROID_PLATFORM_SDK_ID:$notFinalizedValue"
+        }
+
+        val versions = mutableSetOf<String>()
+        // Only include SDK extensions if the symbol has been finalized in at least one
+        if (extensionsSince != ApiElement.NEVER) {
+            for (ext in extensions) {
+                val ident = sdkIdentifiers.find {
+                    it.shortname == ext
+                } ?: throw IllegalStateException("unknown extension SDK \"$ext\"")
+                assert(ident.id != ANDROID_PLATFORM_SDK_ID) // invariant
+                versions.add("${ident.id}:$extensionsSince")
+            }
+        }
+
+        // Only include the Android SDK in `sdks` if
+        // - the symbol has been finalized in an Android dessert, and
+        // - the symbol has been finalized in at least one SDK extension
+        if (androidSince != notFinalizedValue && versions.isNotEmpty()) {
+            versions.add("$ANDROID_PLATFORM_SDK_ID:$androidSince")
+        }
+        return versions.joinToString(",")
+    }
+
+    companion object {
+        // Hard-coded ID for the Android platform SDK. Used identically as the extension SDK IDs
+        // to express when an API first appeared in an SDK.
+        const val ANDROID_PLATFORM_SDK_ID = 0
+
+        private val REGEX_DELIMITERS = Regex("[.#$]")
+
+        /*
+         * Create an ApiToExtensionsMap from a list of text based rules.
+         *
+         * The input is XML:
+         *
+         *     <?xml version="1.0" encoding="utf-8"?>
+         *     <sdk-extensions-info version="1">
+         *         <sdk name="<name>" shortname="<short-name>" id="<int>" reference="<constant>" />
+         *         <symbol jar="<jar>" pattern="<pattern>" sdks="<sdks>" />
+         *     </sdk-extensions-info>
+         *
+         * The <sdk> and <symbol> tags may be repeated.
+         *
+         * - <name> is a long name for the SDK, e.g. "R Extensions".
+         *
+         * - <short-name> is a short name for the SDK, e.g. "R-ext".
+         *
+         * - <id> is the numerical identifier for the SDK, e.g. 30. It is an error to use the
+         *   Android SDK ID (0).
+         *
+         * - <jar> is the jar file symbol belongs to, named after the jar file in
+         *   prebuilts/sdk/extensions/<int>/public, e.g. "framework-sdkextensions".
+         *
+         * - <constant> is a Java symbol that can be passed to `SdkExtensions.getExtensionVersion`
+         *   to look up the version of the corresponding SDK, e.g.
+         *   "android/os/Build$VERSION_CODES$R"
+         *
+         * - <pattern> is either '*', which matches everything, or a 'com.foo.Bar$Inner#member'
+         *   string (or prefix thereof terminated before . or $), which matches anything with that
+         *   prefix. Note that arguments and return values of methods are omitted (and there is no
+         *   way to distinguish overloaded methods).
+         *
+         * - <sdks> is a comma separated list of SDKs in which the symbol defined by <jar> and
+         *   <pattern> appears; the list items are <name> attributes of SDKs defined in the XML.
+         *
+         * It is an error to specify the same <jar> and <pattern> pair twice.
+         *
+         * A more specific <symbol> rule has higher precedence than a less specific rule.
+         *
+         * @param jar jar file to limit lookups to: ignore symbols not present in this jar file
+         * @param xml XML as described above
+         * @throws IllegalArgumentException if the XML is malformed
+         */
+        fun fromXml(filterByJar: String, xml: String): ApiToExtensionsMap {
+            val root = Node("<root>")
+            val sdkIdentifiers = mutableSetOf<SdkIdentifier>()
+            val allSeenExtensions = mutableSetOf<String>()
+
+            val parser = SAXParserFactory.newDefaultInstance().newSAXParser()
+            try {
+                parser.parse(
+                    xml.byteInputStream(),
+                    object : DefaultHandler() {
+                        override fun startElement(uri: String, localName: String, qualifiedName: String, attributes: Attributes) {
+                            when (qualifiedName) {
+                                "sdk" -> {
+                                    val id = attributes.getIntOrThrow(qualifiedName, "id")
+                                    val shortname = attributes.getStringOrThrow(qualifiedName, "shortname")
+                                    val name = attributes.getStringOrThrow(qualifiedName, "name")
+                                    val reference = attributes.getStringOrThrow(qualifiedName, "reference")
+                                    sdkIdentifiers.add(SdkIdentifier(id, shortname, name, reference))
+                                }
+                                "symbol" -> {
+                                    val jar = attributes.getStringOrThrow(qualifiedName, "jar")
+                                    if (jar != filterByJar) {
+                                        return
+                                    }
+                                    val sdks = attributes.getStringOrThrow(qualifiedName, "sdks").split(',')
+                                    if (sdks != sdks.distinct()) {
+                                        throw IllegalArgumentException("symbol lists the same SDK multiple times: '$sdks'")
+                                    }
+                                    allSeenExtensions.addAll(sdks)
+                                    val pattern = attributes.getStringOrThrow(qualifiedName, "pattern")
+                                    if (pattern == "*") {
+                                        root.extensions = sdks
+                                        return
+                                    }
+                                    // add each part of the pattern as separate nodes, e.g. if pattern is
+                                    // com.example.Foo, add nodes, "com" -> "example" -> "Foo"
+                                    val parts = pattern.split(REGEX_DELIMITERS)
+                                    var node = root.children.addNode(parts[0])
+                                    for (name in parts.stream().skip(1)) {
+                                        node = node.children.addNode(name)
+                                    }
+                                    if (node.extensions.isNotEmpty()) {
+                                        throw IllegalArgumentException("duplicate pattern: $pattern")
+                                    }
+                                    node.extensions = sdks
+                                }
+                            }
+                        }
+                    }
+                )
+            } catch (e: Throwable) {
+                throw IllegalArgumentException("failed to parse xml", e)
+            }
+
+            // verify: the predefined Android platform SDK ID is not reused as an extension SDK ID
+            if (sdkIdentifiers.any { it.id == ANDROID_PLATFORM_SDK_ID }) {
+                throw IllegalArgumentException("bad SDK definition: the ID $ANDROID_PLATFORM_SDK_ID is reserved for the Android platform SDK")
+            }
+
+            // verify: all rules refer to declared SDKs
+            val allSdkNames = sdkIdentifiers.map { it.shortname }.toList()
+            for (ext in allSeenExtensions) {
+                if (!allSdkNames.contains(ext)) {
+                    throw IllegalArgumentException("bad SDK definitions: undefined SDK $ext")
+                }
+            }
+
+            // verify: no duplicate SDK IDs
+            if (sdkIdentifiers.size != sdkIdentifiers.distinctBy { it.id }.size) {
+                throw IllegalArgumentException("bad SDK definitions: duplicate SDK IDs")
+            }
+
+            // verify: no duplicate SDK names
+            if (sdkIdentifiers.size != sdkIdentifiers.distinctBy { it.shortname }.size) {
+                throw IllegalArgumentException("bad SDK definitions: duplicate SDK short names")
+            }
+
+            // verify: no duplicate SDK names
+            if (sdkIdentifiers.size != sdkIdentifiers.distinctBy { it.name }.size) {
+                throw IllegalArgumentException("bad SDK definitions: duplicate SDK names")
+            }
+
+            // verify: no duplicate SDK references
+            if (sdkIdentifiers.size != sdkIdentifiers.distinctBy { it.reference }.size) {
+                throw IllegalArgumentException("bad SDK definitions: duplicate SDK references")
+            }
+
+            return ApiToExtensionsMap(sdkIdentifiers, root)
+        }
+    }
+}
+
+private fun MutableSet<Node>.addNode(name: String): Node {
+    findNode(name)?.let {
+        return it
+    }
+    val node = Node(name)
+    add(node)
+    return node
+}
+
+private fun Attributes.getStringOrThrow(tag: String, attr: String): String = getValue(attr) ?: throw IllegalArgumentException("<$tag>: missing attribute: $attr")
+
+private fun Attributes.getIntOrThrow(tag: String, attr: String): Int = getStringOrThrow(tag, attr).toIntOrNull() ?: throw IllegalArgumentException("<$tag>: attribute $attr: not an integer")
+
+private fun Set<Node>.findNode(breadcrumb: String): Node? = find { it.breadcrumb == breadcrumb }
+
+private fun String.toDotNotation(): String = split('(')[0].replace('/', '.')
+
+private class Node(val breadcrumb: String) {
+    var extensions: List<String> = emptyList()
+    val children: MutableSet<Node> = mutableSetOf()
+}
diff --git a/src/main/java/com/android/tools/metalava/apilevels/ExtensionSdkJarReader.kt b/src/main/java/com/android/tools/metalava/apilevels/ExtensionSdkJarReader.kt
new file mode 100644
index 0000000..dc3b360
--- /dev/null
+++ b/src/main/java/com/android/tools/metalava/apilevels/ExtensionSdkJarReader.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2022 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 com.android.tools.metalava.apilevels
+
+import com.android.SdkConstants
+import com.android.SdkConstants.PLATFORM_WINDOWS
+import java.io.File
+
+class ExtensionSdkJarReader() {
+
+    companion object {
+        private val REGEX_JAR_PATH = run {
+            var pattern = ".*/(\\d+)/public/(.*)\\.jar$"
+            if (SdkConstants.currentPlatform() == PLATFORM_WINDOWS) {
+                pattern = pattern.replace("/", "\\\\")
+            }
+            Regex(pattern)
+        }
+
+        /**
+         * Find extension SDK jar files in an extension SDK tree.
+         *
+         * @return a mapping SDK jar file -> list of VersionAndPath objects, sorted from earliest
+         *         to last version
+         */
+        fun findExtensionSdkJarFiles(root: File): Map<String, List<VersionAndPath>> {
+            val map = mutableMapOf<String, MutableList<VersionAndPath>>()
+            root.walk().maxDepth(3).mapNotNull { file ->
+                REGEX_JAR_PATH.matchEntire(file.path)?.groups?.let { groups ->
+                    Triple(groups[2]!!.value, groups[1]!!.value.toInt(), file)
+                }
+            }.sortedBy {
+                it.second
+            }.forEach {
+                map.getOrPut(it.first) {
+                    mutableListOf()
+                }.add(VersionAndPath(it.second, it.third))
+            }
+            return map
+        }
+    }
+}
+
+data class VersionAndPath(@JvmField val version: Int, @JvmField val path: File)
diff --git a/src/main/java/com/android/tools/metalava/apilevels/JarReaderUtils.kt b/src/main/java/com/android/tools/metalava/apilevels/JarReaderUtils.kt
new file mode 100644
index 0000000..58307cb
--- /dev/null
+++ b/src/main/java/com/android/tools/metalava/apilevels/JarReaderUtils.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2022 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 com.android.tools.metalava.apilevels
+
+import com.android.SdkConstants
+import org.objectweb.asm.ClassReader
+import org.objectweb.asm.Opcodes
+import org.objectweb.asm.tree.ClassNode
+import org.objectweb.asm.tree.FieldNode
+import org.objectweb.asm.tree.MethodNode
+import java.io.File
+import java.io.FileInputStream
+import java.util.zip.ZipInputStream
+
+fun Api.readAndroidJar(apiLevel: Int, jar: File) {
+    update(apiLevel)
+    readJar(apiLevel, jar)
+}
+
+fun Api.readExtensionJar(extensionVersion: Int, module: String, jar: File, nextApiLevel: Int) {
+    readJar(nextApiLevel, jar, extensionVersion, module)
+}
+
+fun Api.readJar(apiLevel: Int, jar: File, extensionVersion: Int? = null, module: String? = null) {
+    val fis = FileInputStream(jar)
+    ZipInputStream(fis).use { zis ->
+        var entry = zis.nextEntry
+        while (entry != null) {
+            if (!entry.name.endsWith(SdkConstants.DOT_CLASS)) {
+                entry = zis.nextEntry
+                continue
+            }
+            val bytes = zis.readBytes()
+            val reader = ClassReader(bytes)
+            val classNode = ClassNode(Opcodes.ASM5)
+            reader.accept(classNode, 0)
+
+            val theClass = addClass(
+                classNode.name,
+                apiLevel,
+                (classNode.access and Opcodes.ACC_DEPRECATED) != 0
+            )
+            extensionVersion?.let { theClass.updateExtension(extensionVersion) }
+            module?.let { theClass.updateMainlineModule(module) }
+
+            theClass.updateHidden(
+                apiLevel,
+                (classNode.access and Opcodes.ACC_PUBLIC) == 0
+            )
+
+            // super class
+            if (classNode.superName != null) {
+                theClass.addSuperClass(classNode.superName, apiLevel)
+            }
+
+            // interfaces
+            for (interfaceName in classNode.interfaces) {
+                theClass.addInterface(interfaceName, apiLevel)
+            }
+
+            // fields
+            for (field in classNode.fields) {
+                val fieldNode = field as FieldNode
+                if ((fieldNode.access and (Opcodes.ACC_PUBLIC or Opcodes.ACC_PROTECTED)) == 0) {
+                    continue
+                }
+                if (!fieldNode.name.startsWith("this\$") && fieldNode.name != "\$VALUES") {
+                    val apiField = theClass.addField(
+                        fieldNode.name,
+                        apiLevel,
+                        (fieldNode.access and Opcodes.ACC_DEPRECATED) != 0
+                    )
+                    extensionVersion?.let { apiField.updateExtension(extensionVersion) }
+                }
+            }
+
+            // methods
+            for (method in classNode.methods) {
+                val methodNode = method as MethodNode
+                if ((methodNode.access and (Opcodes.ACC_PUBLIC or Opcodes.ACC_PROTECTED)) == 0) {
+                    continue
+                }
+                if (methodNode.name != "<clinit>") {
+                    val apiMethod = theClass.addMethod(
+                        methodNode.name + methodNode.desc, apiLevel,
+                        (methodNode.access and Opcodes.ACC_DEPRECATED) != 0
+                    )
+                    extensionVersion?.let { apiMethod.updateExtension(extensionVersion) }
+                }
+            }
+
+            entry = zis.nextEntry
+        }
+    }
+}
diff --git a/src/main/java/com/android/tools/metalava/model/AnnotationItem.kt b/src/main/java/com/android/tools/metalava/model/AnnotationItem.kt
index f2e1f25..9159224 100644
--- a/src/main/java/com/android/tools/metalava/model/AnnotationItem.kt
+++ b/src/main/java/com/android/tools/metalava/model/AnnotationItem.kt
@@ -42,7 +42,6 @@
 import com.intellij.psi.PsiReference
 import org.jetbrains.kotlin.psi.KtObjectDeclaration
 import org.jetbrains.uast.UElement
-import java.util.function.Predicate
 
 fun isNullableAnnotation(qualifiedName: String): Boolean {
     return qualifiedName.endsWith("Nullable")
@@ -175,18 +174,20 @@
          * Annotations that should not be exported are mapped to null.
          */
         fun mapName(
-            codebase: Codebase,
             qualifiedName: String?,
-            filter: Predicate<Item>? = null,
             target: AnnotationTarget = AnnotationTarget.SIGNATURE_FILE
         ): String? {
             qualifiedName ?: return null
-            if (options.passThroughAnnotations.contains(qualifiedName)) {
+            if (options.passThroughAnnotations.contains(qualifiedName) ||
+                options.showAnnotations.matches(qualifiedName) ||
+                options.hideAnnotations.matches(qualifiedName)
+            ) {
                 return qualifiedName
             }
             if (options.excludeAnnotations.contains(qualifiedName)) {
                 return null
             }
+
             when (qualifiedName) {
                 // Resource annotations
                 "android.annotation.AnimRes" -> return "androidx.annotation.AnimRes"
@@ -282,21 +283,7 @@
                 "android.annotation.Condemned",
                 "android.annotation.Hide",
 
-                "android.annotation.Widget" -> {
-                    // Remove, unless (a) public or (b) specifically included in --showAnnotations
-                    return if (options.showAnnotations.matches(qualifiedName)) {
-                        qualifiedName
-                    } else if (filter != null) {
-                        val cls = codebase.findClass(qualifiedName)
-                        if (cls != null && filter.test(cls)) {
-                            qualifiedName
-                        } else {
-                            null
-                        }
-                    } else {
-                        qualifiedName
-                    }
-                }
+                "android.annotation.Widget" -> return qualifiedName
 
                 // Included for analysis, but should not be exported:
                 "android.annotation.BroadcastBehavior",
@@ -325,29 +312,10 @@
 
                         // Unknown Android platform annotations
                         qualifiedName.startsWith("android.annotation.") -> {
-                            // Remove, unless specifically included in --showAnnotations
-                            return if (options.showAnnotations.matches(qualifiedName)) {
-                                qualifiedName
-                            } else {
-                                null
-                            }
+                            return null
                         }
 
-                        else -> {
-                            // Remove, unless (a) public or (b) specifically included in --showAnnotations
-                            return if (options.showAnnotations.matches(qualifiedName)) {
-                                qualifiedName
-                            } else if (filter != null) {
-                                val cls = codebase.findClass(qualifiedName)
-                                if (cls != null && filter.test(cls)) {
-                                    qualifiedName
-                                } else {
-                                    null
-                                }
-                            } else {
-                                qualifiedName
-                            }
-                        }
+                        else -> qualifiedName
                     }
                 }
             }
@@ -409,6 +377,14 @@
                 "kotlin.UseExperimental",
                 "kotlin.OptIn" -> return NO_ANNOTATION_TARGETS
 
+                // These optimization-related annotations shouldn't be exported.
+                "dalvik.annotation.optimization.CriticalNative",
+                "dalvik.annotation.optimization.FastNative",
+                "dalvik.annotation.optimization.NeverCompile",
+                "dalvik.annotation.optimization.NeverInline",
+                "dalvik.annotation.optimization.ReachabilitySensitive" ->
+                    return NO_ANNOTATION_TARGETS
+
                 // TODO(aurimas): consider using annotation directly instead of modifiers
                 "kotlin.Deprecated" -> return NO_ANNOTATION_TARGETS // tracked separately as a pseudo-modifier
                 "android.annotation.DeprecatedForSdk",
@@ -474,10 +450,6 @@
             // habit of loading all annotation classes it encounters.)
 
             if (qualifiedName.startsWith("androidx.annotation.")) {
-                if (options.includeSourceRetentionAnnotations) {
-                    return ANNOTATION_IN_ALL_STUBS
-                }
-
                 if (qualifiedName == ANDROIDX_NULLABLE || qualifiedName == ANDROIDX_NONNULL) {
                     // Right now, nullness annotations (other than @RecentlyNullable and @RecentlyNonNull)
                     // have to go in external annotations since they aren't in the class path for
@@ -538,10 +510,14 @@
         fun unshortenAnnotation(source: String): String {
             return when {
                 source == "@Deprecated" -> "@java.lang.Deprecated"
-                // These 3 annotations are in the android.annotation. package, not androidx.annotation
+                // The first 3 annotations are in the android.annotation. package, not androidx.annotation
+                // Nullability annotations are written as @NonNull and @Nullable in API text files,
+                // and these should be linked no android.annotation package when generating stubs.
                 source.startsWith("@SystemService") ||
                     source.startsWith("@TargetApi") ||
-                    source.startsWith("@SuppressLint") ->
+                    source.startsWith("@SuppressLint") ||
+                    source.startsWith("@Nullable") ||
+                    source.startsWith("@NonNull") ->
                     "@android.annotation." + source.substring(1)
                 // If the first character of the name (after "@") is lower-case, then
                 // assume it's a package name, so no need to shorten it.
diff --git a/src/main/java/com/android/tools/metalava/model/ClassItem.kt b/src/main/java/com/android/tools/metalava/model/ClassItem.kt
index 4e11cc3..fa045f1 100644
--- a/src/main/java/com/android/tools/metalava/model/ClassItem.kt
+++ b/src/main/java/com/android/tools/metalava/model/ClassItem.kt
@@ -230,6 +230,10 @@
         return qualifiedName() == JAVA_LANG_OBJECT
     }
 
+    fun isAbstractClass(): Boolean {
+        return this.modifiers.isAbstract()
+    }
+
     // Mutation APIs: Used to "fix up" the API hierarchy (in [ApiAnalyzer]) to only expose
     // visible parts of the API)
 
diff --git a/src/main/java/com/android/tools/metalava/model/Codebase.kt b/src/main/java/com/android/tools/metalava/model/Codebase.kt
index e8a9a58..2ba461e 100644
--- a/src/main/java/com/android/tools/metalava/model/Codebase.kt
+++ b/src/main/java/com/android/tools/metalava/model/Codebase.kt
@@ -57,9 +57,6 @@
      */
     var location: File
 
-    /** The API level of this codebase, or -1 if not known */
-    var apiLevel: Int
-
     /** The packages in the codebase (may include packages that are not included in the API) */
     fun getPackages(): PackageList
 
@@ -269,7 +266,6 @@
     private var minSdkVersion: MinSdkVersion? = null
     override var original: Codebase? = null
     override var units: List<PsiFile> = emptyList()
-    override var apiLevel: Int = -1
     @Suppress("LeakingThis")
     override val printer = CodePrinter(this)
     @Suppress("LeakingThis")
diff --git a/src/main/java/com/android/tools/metalava/model/Item.kt b/src/main/java/com/android/tools/metalava/model/Item.kt
index f309fe4..462403c 100644
--- a/src/main/java/com/android/tools/metalava/model/Item.kt
+++ b/src/main/java/com/android/tools/metalava/model/Item.kt
@@ -299,7 +299,7 @@
             }
             builder.append(' ')
             if (includeReturnValue && !item.isConstructor()) {
-                builder.append(item.returnType()?.toSimpleType())
+                builder.append(item.returnType().toSimpleType())
                 builder.append(' ')
             }
             appendMethodSignature(builder, item, includeParameterNames, includeParameterTypes)
diff --git a/src/main/java/com/android/tools/metalava/model/MethodItem.kt b/src/main/java/com/android/tools/metalava/model/MethodItem.kt
index e4eb38e..fce301a 100644
--- a/src/main/java/com/android/tools/metalava/model/MethodItem.kt
+++ b/src/main/java/com/android/tools/metalava/model/MethodItem.kt
@@ -32,8 +32,8 @@
     /** Whether this method is a constructor */
     fun isConstructor(): Boolean
 
-    /** The type of this field, or null for constructors */
-    fun returnType(): TypeItem?
+    /** The type of this field. Returns the containing class for constructors */
+    fun returnType(): TypeItem
 
     /** The list of parameters */
     fun parameters(): List<ParameterItem>
@@ -71,7 +71,7 @@
         }
 
         sb.append(")")
-        sb.append(if (voidConstructorTypes && isConstructor()) "V" else returnType()?.internalName() ?: "V")
+        sb.append(if (voidConstructorTypes && isConstructor()) "V" else returnType().internalName())
         return sb.toString()
     }
 
@@ -214,9 +214,7 @@
 
         if (!isConstructor()) {
             val type = returnType()
-            if (type != null) { // always true when not a constructor
-                visitor.visitType(type, this)
-            }
+            visitor.visitType(type, this)
         }
 
         for (parameter in parameters()) {
@@ -229,9 +227,7 @@
 
         if (!isConstructor()) {
             val type = returnType()
-            if (type != null) {
-                visitor.visitType(type, this)
-            }
+            visitor.visitType(type, this)
         }
     }
 
@@ -369,7 +365,7 @@
         return when {
             modifiers.hasJvmSyntheticAnnotation() -> false
             isConstructor() -> false
-            (returnType()?.primitive != true) -> true
+            (!returnType().primitive) -> true
             parameters().any { !it.type().primitive } -> true
             else -> false
         }
@@ -380,7 +376,7 @@
             return true
         }
 
-        if (!isConstructor() && returnType()?.primitive != true) {
+        if (!isConstructor() && !returnType().primitive) {
             if (!modifiers.hasNullnessInfo()) {
                 return false
             }
@@ -479,16 +475,14 @@
         }
 
         val returnType = returnType()
-        if (returnType != null) {
-            val returnTypeClass = returnType.asClass()
-            if (returnTypeClass != null && !filterReference.test(returnTypeClass)) {
-                return true
-            }
-            if (returnType.hasTypeArguments()) {
-                for (argument in returnType.typeArgumentClasses()) {
-                    if (!filterReference.test(argument)) {
-                        return true
-                    }
+        val returnTypeClass = returnType.asClass()
+        if (returnTypeClass != null && !filterReference.test(returnTypeClass)) {
+            return true
+        }
+        if (returnType.hasTypeArguments()) {
+            for (argument in returnType.typeArgumentClasses()) {
+                if (!filterReference.test(argument)) {
+                    return true
                 }
             }
         }
diff --git a/src/main/java/com/android/tools/metalava/model/ModifierList.kt b/src/main/java/com/android/tools/metalava/model/ModifierList.kt
index 5b81463..5ea4468 100644
--- a/src/main/java/com/android/tools/metalava/model/ModifierList.kt
+++ b/src/main/java/com/android/tools/metalava/model/ModifierList.kt
@@ -192,7 +192,7 @@
      * in this modifier list
      */
     fun findAnnotation(qualifiedName: String): AnnotationItem? {
-        val mappedName = AnnotationItem.mapName(codebase, qualifiedName)
+        val mappedName = AnnotationItem.mapName(qualifiedName)
         return annotations().firstOrNull {
             mappedName == it.qualifiedName
         }
diff --git a/src/main/java/com/android/tools/metalava/model/TypeItem.kt b/src/main/java/com/android/tools/metalava/model/TypeItem.kt
index 8084dca..2787972 100644
--- a/src/main/java/com/android/tools/metalava/model/TypeItem.kt
+++ b/src/main/java/com/android/tools/metalava/model/TypeItem.kt
@@ -95,6 +95,15 @@
         return s
     }
 
+    /**
+     * Returns the element type if the type is an array or contains a vararg.
+     * If the element is not an array or does not contain a vararg,
+     * returns the original type string.
+     */
+    fun toElementType(): String {
+        return toTypeString().replace("...", "").replace("[]", "")
+    }
+
     val primitive: Boolean
 
     fun typeArgumentClasses(): List<ClassItem>
@@ -127,13 +136,10 @@
     fun defaultValue(): Any? {
         return when (toTypeString()) {
             "boolean" -> false
+            "char", "int", "float", "double" -> 0
             "byte" -> 0.toByte()
-            "char" -> '\u0000'
             "short" -> 0.toShort()
-            "int" -> 0
             "long" -> 0L
-            "float" -> 0f
-            "double" -> 0.0
             else -> null
         }
     }
@@ -143,6 +149,53 @@
     fun hasTypeArguments(): Boolean = toTypeString().contains("<")
 
     /**
+     * If the item has type arguments, return a list of type arguments.
+     * If simplified is true, returns the simplified forms of the type arguments.
+     * e.g. when type arguments are <K, V extends some.arbitrary.Class>, [K, V] will be returned.
+     * If the item does not have any type arguments, return an empty list.
+     */
+    fun typeArguments(simplified: Boolean = false): List<String> {
+        if (!hasTypeArguments()) {
+            return emptyList()
+        }
+        val typeString = toTypeString()
+        val bracketRemovedTypeString = typeString.indexOf('<')
+            .let { typeString.substring(it + 1, typeString.length - 1) }
+        val typeArguments = mutableListOf<String>()
+        var builder = StringBuilder()
+        var balance = 0
+        var idx = 0
+        while (idx < bracketRemovedTypeString.length) {
+            when (val s = bracketRemovedTypeString[idx]) {
+                ',' -> {
+                    if (balance == 0) {
+                        typeArguments.add(builder.toString())
+                        builder = StringBuilder()
+                    } else {
+                        builder.append(s)
+                    }
+                }
+                '<' -> {
+                    balance += 1
+                    builder.append(s)
+                }
+                '>' -> {
+                    balance -= 1
+                    builder.append(s)
+                }
+                else -> builder.append(s)
+            }
+            idx += 1
+        }
+        typeArguments.add(builder.toString())
+
+        if (simplified) {
+            return typeArguments.map { it.substringBefore(" extends ").trim() }
+        }
+        return typeArguments.map { it.trim() }
+    }
+
+    /**
      * If this type is a type parameter, then return the corresponding [TypeParameterItem].
      * The optional [context] provides the method or class where this type parameter
      * appears, and can be used for example to resolve the bounds for a type variable
diff --git a/src/main/java/com/android/tools/metalava/model/psi/PsiAnnotationItem.kt b/src/main/java/com/android/tools/metalava/model/psi/PsiAnnotationItem.kt
index 4077b2a..5de4137 100644
--- a/src/main/java/com/android/tools/metalava/model/psi/PsiAnnotationItem.kt
+++ b/src/main/java/com/android/tools/metalava/model/psi/PsiAnnotationItem.kt
@@ -51,7 +51,7 @@
     val psiAnnotation: PsiAnnotation,
     override val originalName: String?
 ) : DefaultAnnotationItem(codebase) {
-    override val qualifiedName: String? = AnnotationItem.mapName(codebase, originalName)
+    override val qualifiedName: String? = AnnotationItem.mapName(originalName)
 
     override fun toString(): String = toSource()
 
@@ -137,7 +137,7 @@
             target: AnnotationTarget,
             showDefaultAttrs: Boolean
         ) {
-            val qualifiedName = AnnotationItem.mapName(codebase, originalName, null, target) ?: return
+            val qualifiedName = AnnotationItem.mapName(originalName, target) ?: return
 
             val attributes = getAttributes(psiAnnotation, showDefaultAttrs)
             if (attributes.isEmpty()) {
diff --git a/src/main/java/com/android/tools/metalava/model/psi/PsiBasedCodebase.kt b/src/main/java/com/android/tools/metalava/model/psi/PsiBasedCodebase.kt
index 74b35d4..f782d27 100644
--- a/src/main/java/com/android/tools/metalava/model/psi/PsiBasedCodebase.kt
+++ b/src/main/java/com/android/tools/metalava/model/psi/PsiBasedCodebase.kt
@@ -40,6 +40,7 @@
 import com.intellij.psi.PsiClass
 import com.intellij.psi.PsiClassOwner
 import com.intellij.psi.PsiClassType
+import com.intellij.psi.PsiCodeBlock
 import com.intellij.psi.PsiElement
 import com.intellij.psi.PsiErrorElement
 import com.intellij.psi.PsiField
@@ -169,7 +170,9 @@
         for (psiFile in psiFiles.asSequence().distinct()) {
             tick() // show progress
 
-            psiFile.accept(object : JavaRecursiveElementVisitor() {
+            // Visiting psiFile directly would eagerly load the entire file even though we only need
+            // the importList here.
+            (psiFile as? PsiJavaFile)?.importList?.accept(object : JavaRecursiveElementVisitor() {
                 override fun visitImportStatement(element: PsiImportStatement) {
                     super.visitImportStatement(element)
                     if (element.resolve() == null) {
@@ -227,6 +230,15 @@
                                     "Syntax error: `${element.errorDescription}`"
                                 )
                             }
+
+                            override fun visitCodeBlock(block: PsiCodeBlock?) {
+                                // Ignore to avoid eagerly parsing all method bodies.
+                            }
+
+                            override fun visitDocComment(comment: PsiDocComment?) {
+                                // Ignore to avoid eagerly parsing all doc comments.
+                                // Doc comments cannot contain error elements.
+                            }
                         })
 
                         topLevelClassesFromSource += createClass(psiClass)
diff --git a/src/main/java/com/android/tools/metalava/model/psi/PsiItem.kt b/src/main/java/com/android/tools/metalava/model/psi/PsiItem.kt
index df9fd1a..0277c4f 100644
--- a/src/main/java/com/android/tools/metalava/model/psi/PsiItem.kt
+++ b/src/main/java/com/android/tools/metalava/model/psi/PsiItem.kt
@@ -167,7 +167,7 @@
         // API target (there are many), and each time would have involved constructing a full javadoc
         // AST with lexical tokens using IntelliJ's javadoc parsing APIs. Instead, we'll just
         // do some simple string heuristics.
-        if (tagSection == "@apiSince" || tagSection == "@deprecatedSince") {
+        if (tagSection == "@apiSince" || tagSection == "@deprecatedSince" || tagSection == "@sdkExtSince") {
             documentation = addUniqueTag(documentation, tagSection, comment)
             return
         }
diff --git a/src/main/java/com/android/tools/metalava/model/psi/PsiMethodItem.kt b/src/main/java/com/android/tools/metalava/model/psi/PsiMethodItem.kt
index 167173f..52ce6b1 100644
--- a/src/main/java/com/android/tools/metalava/model/psi/PsiMethodItem.kt
+++ b/src/main/java/com/android/tools/metalava/model/psi/PsiMethodItem.kt
@@ -110,7 +110,7 @@
 
     override fun isImplicitConstructor(): Boolean = false
 
-    override fun returnType(): TypeItem? = returnType
+    override fun returnType(): TypeItem = returnType
 
     override fun parameters(): List<PsiParameterItem> = parameters
 
@@ -316,7 +316,7 @@
         }
 
         val returnType = method.returnType()
-        sb.append(returnType?.convertTypeString(replacementMap))
+        sb.append(returnType.convertTypeString(replacementMap))
 
         sb.append(' ')
         sb.append(method.name())
diff --git a/src/main/java/com/android/tools/metalava/model/psi/PsiTypePrinter.kt b/src/main/java/com/android/tools/metalava/model/psi/PsiTypePrinter.kt
index 0b99b35..3c61a88 100644
--- a/src/main/java/com/android/tools/metalava/model/psi/PsiTypePrinter.kt
+++ b/src/main/java/com/android/tools/metalava/model/psi/PsiTypePrinter.kt
@@ -488,7 +488,7 @@
 
         val mapped =
             if (mapAnnotations) {
-                AnnotationItem.mapName(codebase, qualifiedName) ?: return null
+                AnnotationItem.mapName(qualifiedName) ?: return null
             } else {
                 qualifiedName
             }
diff --git a/src/main/java/com/android/tools/metalava/model/psi/UAnnotationItem.kt b/src/main/java/com/android/tools/metalava/model/psi/UAnnotationItem.kt
index c76dfcf..45402e7 100644
--- a/src/main/java/com/android/tools/metalava/model/psi/UAnnotationItem.kt
+++ b/src/main/java/com/android/tools/metalava/model/psi/UAnnotationItem.kt
@@ -51,7 +51,7 @@
     val uAnnotation: UAnnotation,
     override val originalName: String?
 ) : DefaultAnnotationItem(codebase) {
-    override val qualifiedName: String? = AnnotationItem.mapName(codebase, originalName)
+    override val qualifiedName: String? = AnnotationItem.mapName(originalName)
 
     override fun toString(): String = toSource()
 
@@ -121,7 +121,7 @@
             target: AnnotationTarget,
             showDefaultAttrs: Boolean
         ) {
-            val qualifiedName = AnnotationItem.mapName(codebase, originalName, null, target) ?: return
+            val qualifiedName = AnnotationItem.mapName(originalName, target) ?: return
 
             val attributes = getAttributes(uAnnotation, showDefaultAttrs)
             if (attributes.isEmpty()) {
diff --git a/src/main/java/com/android/tools/metalava/model/text/ApiFile.kt b/src/main/java/com/android/tools/metalava/model/text/ApiFile.kt
index 5766605..df1bc61 100644
--- a/src/main/java/com/android/tools/metalava/model/text/ApiFile.kt
+++ b/src/main/java/com/android/tools/metalava/model/text/ApiFile.kt
@@ -47,11 +47,7 @@
      * Even if false, we'll allow them if the file format supports them/
      */
     @Throws(ApiParseException::class)
-    fun parseApi(@Nonnull file: File, kotlinStyleNulls: Boolean): TextCodebase {
-        val files: MutableList<File> = ArrayList(1)
-        files.add(file)
-        return parseApi(files, kotlinStyleNulls)
-    }
+    fun parseApi(@Nonnull file: File, kotlinStyleNulls: Boolean) = parseApi(listOf(file), kotlinStyleNulls)
 
     /**
      * Read API signature files into a [TextCodebase].
@@ -242,9 +238,7 @@
         var isInterface = false
         var isAnnotation = false
         var isEnum = false
-        val qualifiedName: String
         var ext: String? = null
-        val cl: TextClassItem
 
         // Metalava: including annotations in file now
         val annotations: List<String> = getAnnotations(tokenizer, token)
@@ -279,10 +273,7 @@
         }
         assertIdent(tokenizer, token)
         val name: String = token
-        qualifiedName = qualifiedName(pkg.name(), name)
-        if (api.findClass(qualifiedName) != null) {
-            throw ApiParseException("Duplicate class found: $qualifiedName", tokenizer)
-        }
+        val qualifiedName = qualifiedName(pkg.name(), name)
         val typeInfo = api.obtainTypeFromString(qualifiedName)
         // Simple type info excludes the package name (but includes enclosing class names)
         var rawName = name
@@ -291,11 +282,22 @@
             rawName = rawName.substring(0, variableIndex)
         }
         token = tokenizer.requireToken()
-        cl = TextClassItem(
+        val maybeExistingClass = TextClassItem(
             api, tokenizer.pos(), modifiers, isInterface, isEnum, isAnnotation,
             typeInfo.toErasedTypeString(null), typeInfo.qualifiedTypeName(),
             rawName, annotations
         )
+        val cl = when (val foundClass = api.findClass(maybeExistingClass.qualifiedName())) {
+            null -> maybeExistingClass
+            else -> {
+                if (!foundClass.isCompatible(maybeExistingClass)) {
+                    throw ApiParseException("Incompatible $foundClass definitions")
+                } else {
+                    foundClass
+                }
+            }
+        }
+
         cl.setContainingPackage(pkg)
         cl.setTypeInfo(typeInfo)
         cl.deprecated = modifiers.isDeprecated()
@@ -446,12 +448,17 @@
     ) {
         var token = startingToken
         val method: TextConstructorItem
+        var typeParameterList = NONE
 
         // Metalava: including annotations in file now
         val annotations: List<String> = getAnnotations(tokenizer, token)
         token = tokenizer.current
         val modifiers = parseModifiers(api, tokenizer, token, annotations)
         token = tokenizer.current
+        if ("<" == token) {
+            typeParameterList = parseTypeParameterList(api, tokenizer)
+            token = tokenizer.requireToken()
+        }
         assertIdent(tokenizer, token)
         val name: String = token.substring(token.lastIndexOf('.') + 1) // For inner classes, strip outer classes from name
         token = tokenizer.requireToken()
@@ -461,6 +468,10 @@
         method = TextConstructorItem(api, name, cl, modifiers, cl.asTypeInfo(), tokenizer.pos())
         method.deprecated = modifiers.isDeprecated()
         parseParameterList(api, tokenizer, method)
+        method.setTypeParameterList(typeParameterList)
+        if (typeParameterList is TextTypeParameterList) {
+            typeParameterList.owner = method
+        }
         token = tokenizer.requireToken()
         if ("throws" == token) {
             token = parseThrows(tokenizer, method)
@@ -551,7 +562,9 @@
         if (";" != token) {
             throw ApiParseException("expected ; found $token", tokenizer)
         }
-        cl.addMethod(method)
+        if (!cl.methods().contains(method)) {
+            cl.addMethod(method)
+        }
     }
 
     private fun mergeAnnotations(
diff --git a/src/main/java/com/android/tools/metalava/model/text/TextBackedAnnotationItem.kt b/src/main/java/com/android/tools/metalava/model/text/TextBackedAnnotationItem.kt
index fce3a9d..b3bd1fc 100644
--- a/src/main/java/com/android/tools/metalava/model/text/TextBackedAnnotationItem.kt
+++ b/src/main/java/com/android/tools/metalava/model/text/TextBackedAnnotationItem.kt
@@ -40,7 +40,7 @@
         else source.substring(1, index)
 
         originalName = annotationClass
-        qualifiedName = if (mapName) AnnotationItem.mapName(codebase, annotationClass) else annotationClass
+        qualifiedName = if (mapName) AnnotationItem.mapName(annotationClass) else annotationClass
         full = when {
             qualifiedName == null -> ""
             index == -1 -> "@$qualifiedName"
diff --git a/src/main/java/com/android/tools/metalava/model/text/TextClassItem.kt b/src/main/java/com/android/tools/metalava/model/text/TextClassItem.kt
index dffe723..7c3a121 100644
--- a/src/main/java/com/android/tools/metalava/model/text/TextClassItem.kt
+++ b/src/main/java/com/android/tools/metalava/model/text/TextClassItem.kt
@@ -24,6 +24,7 @@
 import com.android.tools.metalava.model.Item
 import com.android.tools.metalava.model.MethodItem
 import com.android.tools.metalava.model.PackageItem
+import com.android.tools.metalava.model.ParameterItem
 import com.android.tools.metalava.model.PropertyItem
 import com.android.tools.metalava.model.TypeItem
 import com.android.tools.metalava.model.TypeParameterItem
@@ -225,6 +226,22 @@
         innerClasses.add(cls)
     }
 
+    fun isCompatible(cls: TextClassItem): Boolean {
+        if (this === cls) {
+            return true
+        }
+        if (fullName != cls.fullName) {
+            return false
+        }
+
+        return modifiers.toString() == cls.modifiers.toString() &&
+            isInterface == cls.isInterface &&
+            isEnum == cls.isEnum &&
+            isAnnotation == cls.isAnnotation &&
+            superClass == cls.superClass &&
+            allInterfaces().toSet() == cls.allInterfaces().toSet()
+    }
+
     override fun filteredSuperClassType(predicate: Predicate<Item>): TypeItem? {
         // No filtering in signature files: we assume signature APIs
         // have already been filtered and all items should match.
@@ -260,6 +277,69 @@
         return emptyMap()
     }
 
+    override fun createDefaultConstructor(): ConstructorItem {
+        return TextConstructorItem.createDefaultConstructor(codebase, this, position)
+    }
+
+    fun containsMethodInClassContext(method: MethodItem): Boolean {
+        return methods.any { equalMethodInClassContext(it, method) }
+    }
+
+    fun getParentAndInterfaces(): List<TextClassItem> {
+        val classes = interfaceTypes().map { it.asClass() as TextClassItem }.toMutableList()
+        superClass()?.let { classes.add(0, it as TextClassItem) }
+        return classes
+    }
+
+    private var allSuperClassesAndInterfaces: List<TextClassItem>? = null
+
+    /** Returns all super classes and interfaces in the class hierarchy the class item inherits.
+     * The returned list is sorted by the proximity of the classes to the class item in the hierarchy chain.
+     * If an interface appears multiple time in the hierarchy chain,
+     * it is ordered based on the furthest distance to the class item.
+     */
+    fun getAllSuperClassesAndInterfaces(): List<TextClassItem> {
+        allSuperClassesAndInterfaces?.let { return it }
+
+        val classLevelMap = mutableMapOf<TextClassItem, Int>()
+
+        // Stores the parent class and interfaces to be iterated.
+        // Since a class can inherit multiple class and interfaces, queue is two-dimensional.
+        // Each inner lists represents all super class and interfaces in the same hierarchy level.
+        val queue = ArrayDeque<List<TextClassItem>>()
+        queue.add(getParentAndInterfaces())
+
+        // We need to visit the hierarchy starting from the greatest ancestor,
+        // but we cannot naively reverse-iterate based on the order the hierarchy is discovered
+        // because a class/interface can appear multiple times in the hierarchy graph
+        // (i.e. a vertex can have multiple outgoing edges).
+        // Thus, we keep track of the furthest distances from each hierarchy vertices to the
+        // destination vertex (cl) and reverse iterate from the vertices that are
+        // farthest from the destination.
+        var hierarchyLevel = 1
+        while (queue.isNotEmpty()) {
+            val superClasses = queue.removeFirst()
+            val parentClasses = ArrayList<TextClassItem>()
+            for (superClass in superClasses) {
+                // Every class extends java.lang.Object and thus not need to be
+                // included in the hierarchy
+                if (!superClass.isJavaLangObject()) {
+                    classLevelMap[superClass] = hierarchyLevel
+                    parentClasses.addAll(superClass.getParentAndInterfaces())
+                }
+            }
+            if (parentClasses.isNotEmpty()) {
+                queue.add(parentClasses)
+            }
+            hierarchyLevel += 1
+        }
+
+        allSuperClassesAndInterfaces =
+            classLevelMap.toList().sortedWith(compareBy { it.second }).map { it.first }
+
+        return allSuperClassesAndInterfaces!!
+    }
+
     companion object {
         fun createClassStub(codebase: TextCodebase, name: String): TextClassItem =
             createStub(codebase, name, isInterface = false)
@@ -304,5 +384,215 @@
 
             return qualifiedName.substring(qualifiedName.lastIndexOf('.') + 1)
         }
+
+        /**
+         * Determines whether if [thisClassType] is covariant type with [otherClassType].
+         * If [thisClassType] does not belong to this class, return false.
+         *
+         * @param thisClass [ClassItem] that [thisClassType] belongs to
+         * @param thisClassType [TypeItem] that belongs to this class
+         * @param otherClassType [TypeItem] that belongs to other class
+         * @return Boolean that indicates whether if the two types are covariant
+         */
+        private fun isCovariantType(
+            thisClass: ClassItem,
+            thisClassType: TypeItem,
+            otherClassType: TypeItem
+        ): Boolean {
+            // TypeItem.asClass() returns null for primitive types.
+            // Since primitive types are not covariant with anything, return false
+            val otherClass = otherClassType.asClass() ?: return false
+
+            val otherSuperClassNames = (otherClass as TextClassItem)
+                .getAllSuperClassesAndInterfaces().map { it.qualifiedName() }
+
+            val thisClassTypeErased = thisClassType.toErasedTypeString()
+            val typeArgIndex = thisClass.toType().typeArguments(simplified = true).indexOf(thisClassTypeErased)
+
+            // thisClassSuperType is the super type of thisClassType retrieved from the type arguments.
+            // e.g. when type arguments are <K, V extends some.arbitrary.Class>,
+            // thisClassSuperType will be "some.arbitrary.Class" when the thisClassType is "V"
+            // If thisClassType is not included in the type arguments or
+            // if thisClassType does not have a super type specified in the type argument,
+            // thisClassSuperType will be thisClassType.
+            val thisClassSuperType = if (typeArgIndex == -1) thisClassTypeErased else
+                thisClass.toType().typeArguments()[typeArgIndex].substringAfterLast(" extends ")
+
+            return thisClassSuperType in otherSuperClassNames
+        }
+
+        private fun hasEqualTypeVar(
+            type1: TypeItem,
+            class1: ClassItem,
+            type2: TypeItem,
+            class2: ClassItem
+        ): Boolean {
+
+            // Given a type and its containing class,
+            // find the interface types that contains the type.
+            // For instance, for a method that looks like:
+            // class SomeClass implements InterfaceA<some.return.Type>, InterfaceB<some.return.Type>
+            //     Type foo()
+            // this function will return [InterfaceA, InterfaceB] when Type and SomeClass
+            // are passed as inputs.
+            val typeContainingInterfaces = {
+                t: TypeItem, cl: ClassItem ->
+                val interfaceTypes = cl.interfaceTypes()
+                    .plus(cl.toType())
+                    .plus(cl.superClassType())
+                    .filterNotNull()
+                interfaceTypes.filter {
+                    val typeArgs = it.typeArguments(simplified = true)
+                    t.toString() in typeArgs ||
+                        t.toElementType() in typeArgs ||
+                        t.asClass()?.superClass()?.qualifiedName() in typeArgs
+                }
+            }
+
+            val typeContainingInterfaces1 = typeContainingInterfaces(type1, class1)
+            val typeContainingInterfaces2 = typeContainingInterfaces(type2, class2)
+
+            if (typeContainingInterfaces1.isEmpty() || typeContainingInterfaces2.isEmpty()) {
+                return false
+            }
+
+            val interfaceTypesAreCovariant = {
+                t1: TypeItem, t2: TypeItem ->
+                t1.toErasedTypeString() == t2.toErasedTypeString() ||
+                    t1.asClass()?.superClass()?.qualifiedName() == t2.asClass()?.qualifiedName() ||
+                    t2.asClass()?.superClass()?.qualifiedName() == t1.asClass()?.qualifiedName()
+            }
+
+            // Check if the return type containing interfaces of the two methods have an intersection.
+            return typeContainingInterfaces1.any {
+                typeInterface1 ->
+                typeContainingInterfaces2.any {
+                    typeInterface2 ->
+                    interfaceTypesAreCovariant(typeInterface1, typeInterface2)
+                }
+            }
+        }
+
+        private fun hasEqualTypeBounds(method1: MethodItem, method2: MethodItem): Boolean {
+            val typeInTypeParams = {
+                t: TypeItem, m: MethodItem ->
+                t in m.typeParameterList().typeParameters().map { it.toType() }
+            }
+
+            val getTypeBounds = {
+                t: TypeItem, m: MethodItem ->
+                m.typeParameterList().typeParameters().single { it.toType() == t }.typeBounds().toSet()
+            }
+
+            val returnType1 = method1.returnType()
+            val returnType2 = method2.returnType()
+
+            // The following two methods are considered equal:
+            // method public <A extends some.package.SomeClass> A foo (Class<A>);
+            // method public <T extends some.package.SomeClass> T foo (Class<T>);
+            // This can be verified by converting return types to bounds ([some.package.SomeClass])
+            // and compare equivalence.
+            return typeInTypeParams(returnType1, method1) && typeInTypeParams(returnType2, method2) &&
+                getTypeBounds(returnType1, method1) == getTypeBounds(returnType2, method2)
+        }
+
+        private fun hasCovariantTypes(
+            type1: TypeItem,
+            class1: ClassItem,
+            type2: TypeItem,
+            class2: ClassItem
+        ): Boolean {
+            val types = listOf(type1, type2)
+
+            val type1Erased = type1.toErasedTypeString()
+            val type2Erased = type2.toErasedTypeString()
+
+            // The return type of the following two methods are considered equal:
+            // when SomeReturnSubClass extends SomeReturnClass:
+            // method SomeReturnClass foo() and method SomeReturnSubClass foo()
+            // Likewise, the return type of the two methods are also considered equal:
+            // method T foo() in SomeClass<T extends SomeReturnClass> and
+            // method SomeReturnSubClass foo() in SomeOtherClass
+            // This can be verified by checking if a method's return type exists in
+            // another method return type's super classes
+            // Since this method is only used to compare methods with same name and parameters count
+            // within same hierarchy tree, it is unlikely that
+            // two methods have same generic return type.
+            // However, not comparing erased type strings may lead to false negatives
+            // (e.g. comparing java.util.Iterator<E> and java.util.Iterator<T>)
+            // Thus erased type strings equivalence must be evaluated.
+            return type1Erased == type2Erased ||
+                isCovariantType(class1, type1, type2) ||
+                isCovariantType(class2, type2, type1) ||
+                types.any { it.isJavaLangObject() }
+        }
+
+        /**
+         * Compares two [MethodItem]s and determines if the two methods have equal return types.
+         * The two methods' return types are considered equal even if the two are not identical,
+         * but are compatible in compiler level. For instance, return types in a same hierarchy tree
+         * are considered equal.
+         *
+         * @param method1 first [MethodItem] to compare the return type
+         * @param method2 second [MethodItem] to compare the return type
+         * @return a [Boolean] value representing if the two methods' return types are equal
+         */
+        fun hasEqualReturnType(method1: MethodItem, method2: MethodItem): Boolean {
+            val returnType1 = method1.returnType()
+            val returnType2 = method2.returnType()
+            val class1 = method1.containingClass()
+            val class2 = method2.containingClass()
+
+            if (returnType1 == returnType2) return true
+
+            if (hasEqualTypeVar(returnType1, class1, returnType2, class2)) return true
+
+            if (hasEqualTypeBounds(method1, method2)) return true
+
+            if (hasCovariantTypes(returnType1, class1, returnType2, class2)) return true
+
+            return false
+        }
+
+        /**
+         * Compares two [MethodItem] and determines if the two are considered equal based on
+         * the context of the containing classes of the methods. To be specific, for the two methods
+         * in which the coexistence in a class would lead to a
+         * method already defined compiler error, this method returns true.
+         *
+         * @param method1 first [MethodItem] to compare
+         * @param method2 second [MethodItem] to compare
+         * @return a [Boolean] value representing if the two methods are equal or not
+         * with respect to the classes contexts.
+         */
+        fun equalMethodInClassContext(method1: MethodItem, method2: MethodItem): Boolean {
+            if (method1 == method2) return true
+
+            if (method1.name() != method2.name()) return false
+            if (method1.parameters().size != method2.parameters().size) return false
+
+            val hasEqualParams = method1.parameters().zip(method2.parameters()).all {
+                (param1, param2): Pair<ParameterItem, ParameterItem> ->
+                val type1 = param1.type()
+                val type2 = param2.type()
+                val class1 = method1.containingClass()
+                val class2 = method2.containingClass()
+
+                // At this point, two methods' return types equivalence would have been checked.
+                // i.e. If hasEqualReturnType(method1, method2) is true,
+                // we know that the two methods return types are equal.
+                // In other words, if the two compared param types are both method return types,
+                // we transitively know that the two param types are equal as well.
+                val bothAreMethodReturnType =
+                    type1 == method1.returnType() && type2 == method2.returnType()
+
+                type1 == type2 ||
+                    bothAreMethodReturnType ||
+                    hasEqualTypeVar(type1, class1, type2, class2) ||
+                    hasCovariantTypes(type1, class1, type2, class2)
+            }
+
+            return hasEqualReturnType(method1, method2) && hasEqualParams
+        }
     }
 }
diff --git a/src/main/java/com/android/tools/metalava/model/text/TextCodebase.kt b/src/main/java/com/android/tools/metalava/model/text/TextCodebase.kt
index 815d688..384f8f3 100644
--- a/src/main/java/com/android/tools/metalava/model/text/TextCodebase.kt
+++ b/src/main/java/com/android/tools/metalava/model/text/TextCodebase.kt
@@ -39,6 +39,7 @@
 import com.android.tools.metalava.model.TypeParameterList
 import com.android.tools.metalava.model.visitors.ItemVisitor
 import com.android.tools.metalava.model.visitors.TypeVisitor
+import com.android.tools.metalava.options
 import java.io.File
 import java.util.ArrayList
 import java.util.HashMap
@@ -104,7 +105,9 @@
         if (!mClassToInterface.containsKey(classInfo)) {
             mClassToInterface[classInfo] = ArrayList()
         }
-        mClassToInterface[classInfo]?.add(iface)
+        mClassToInterface[classInfo]?.let {
+            if (!it.contains(iface)) it.add(iface)
+        }
     }
 
     fun implementsInterface(classInfo: TextClassItem, iface: String): Boolean {
@@ -206,6 +209,89 @@
         }
     }
 
+    /**
+     * Add abstract superclass abstract methods to non-abstract class
+     * when generating from-text stubs.
+     * Iterate through the hierarchy and collect all super abstract methods that need to be added.
+     * These are not included in the signature files but omitting these methods
+     * will lead to compile error.
+     */
+    private fun resolveAbstractMethods(allClasses: List<TextClassItem>) {
+        for (cl in allClasses) {
+
+            // If class is interface, naively iterate through all parent class and interfaces
+            // and resolve inheritance of override equivalent signatures
+            // Find intersection of super class/interface default methods
+            // Resolve conflict by adding signature
+            // https://docs.oracle.com/javase/specs/jls/se8/html/jls-9.html#jls-9.4.1.3
+            if (cl.isInterface()) {
+
+                // We only need to track one method item(value) with the signature(key),
+                // since the containing class does not matter if a method to be added is found
+                // as method.duplicate(cl) sets containing class to cl.
+                // Therefore, the value of methodMap can be overwritten.
+                val methodMap = mutableMapOf<String, TextMethodItem>()
+                val methodCount = mutableMapOf<String, Int>()
+                val hasDefault = mutableMapOf<String, Boolean>()
+                for (superInterfaceOrClass in cl.getParentAndInterfaces()) {
+                    val methods = superInterfaceOrClass.methods().map { it as TextMethodItem }
+                    for (method in methods) {
+                        val signature = method.toSignatureString()
+                        val isDefault = method.modifiers.isDefault()
+                        val newCount = methodCount.getOrDefault(signature, 0) + 1
+                        val newHasDefault = hasDefault.getOrDefault(signature, false) || isDefault
+
+                        methodMap[signature] = method
+                        methodCount[signature] = newCount
+                        hasDefault[signature] = newHasDefault
+
+                        // If the method has appeared more than once, there may be a potential conflict
+                        // thus add the method to the interface
+                        if (newHasDefault && newCount == 2 &&
+                            !cl.containsMethodInClassContext(method)
+                        ) {
+                            val m = method.duplicate(cl) as TextMethodItem
+                            m.modifiers.setAbstract(true)
+                            m.modifiers.setDefault(false)
+                            cl.addMethod(m)
+                        }
+                    }
+                }
+            }
+
+            // If class is a concrete class, iterate through all hierarchy and
+            // find all missing abstract methods.
+            // Only add methods that are not implemented in the hierarchy and not included
+            else if (!cl.isAbstractClass() && !cl.isEnum()) {
+                val superMethodsToBeOverridden = mutableListOf<TextMethodItem>()
+                val hierarchyClassesList = cl.getAllSuperClassesAndInterfaces().toMutableList()
+                while (hierarchyClassesList.isNotEmpty()) {
+                    val ancestorClass = hierarchyClassesList.removeLast()
+                    val abstractMethods = ancestorClass.methods().filter { it.modifiers.isAbstract() }
+                    for (method in abstractMethods) {
+                        // We do not compare this against all ancestors of cl,
+                        // because an abstract method cannot be overridden at its ancestor class.
+                        // Thus, we compare against hierarchyClassesList.
+                        if (hierarchyClassesList.all { !it.containsMethodInClassContext(method) } &&
+                            !cl.containsMethodInClassContext(method)
+                        ) {
+                            superMethodsToBeOverridden.add(method as TextMethodItem)
+                        }
+                    }
+                }
+                for (superMethod in superMethodsToBeOverridden) {
+                    // MethodItem.duplicate() sets the containing class of
+                    // the duplicated method item as the input parameter.
+                    // Thus, the method items to be overridden are duplicated here after the
+                    // ancestor classes iteration so that the method items are correctly compared.
+                    val m = superMethod.duplicate(cl) as TextMethodItem
+                    m.modifiers.setAbstract(false)
+                    cl.addMethod(m)
+                }
+            }
+        }
+    }
+
     fun registerClass(cls: TextClassItem) {
         mAllClasses[cls.qualifiedName] = cls
     }
@@ -261,6 +347,12 @@
         resolveInterfaces(classes)
         resolveThrowsClasses(classes)
         resolveInnerClasses(packages)
+
+        // Add overridden methods to the codebase only when the codebase is generated
+        // from text file passed via --source-files
+        if (this.location in options.sources) {
+            resolveAbstractMethods(classes)
+        }
     }
 
     override fun findPackage(pkgName: String): TextPackageItem? {
diff --git a/src/main/java/com/android/tools/metalava/model/text/TextConstructorItem.kt b/src/main/java/com/android/tools/metalava/model/text/TextConstructorItem.kt
index d854695..4577801 100644
--- a/src/main/java/com/android/tools/metalava/model/text/TextConstructorItem.kt
+++ b/src/main/java/com/android/tools/metalava/model/text/TextConstructorItem.kt
@@ -17,13 +17,14 @@
 package com.android.tools.metalava.model.text
 
 import com.android.tools.metalava.model.ConstructorItem
+import com.android.tools.metalava.model.DefaultModifierList
 
 class TextConstructorItem(
     codebase: TextCodebase,
     name: String,
     containingClass: TextClassItem,
     modifiers: TextModifiers,
-    returnType: TextTypeItem?,
+    returnType: TextTypeItem,
     position: SourcePositionInfo
 ) : TextMethodItem(codebase, name, containingClass, modifiers, returnType, position),
     ConstructorItem {
@@ -31,4 +32,26 @@
     override var superConstructor: ConstructorItem? = null
 
     override fun isConstructor(): Boolean = true
+
+    companion object {
+        fun createDefaultConstructor(
+            codebase: TextCodebase,
+            containingClass: TextClassItem,
+            position: SourcePositionInfo,
+        ): TextConstructorItem {
+            val name = containingClass.name
+            val modifiers = TextModifiers(codebase, DefaultModifierList.PACKAGE_PRIVATE, null)
+
+            val item = TextConstructorItem(
+                codebase = codebase,
+                name = name,
+                containingClass = containingClass,
+                modifiers = modifiers,
+                returnType = containingClass.asTypeInfo(),
+                position = position,
+            )
+            modifiers.setOwner(item)
+            return item
+        }
+    }
 }
diff --git a/src/main/java/com/android/tools/metalava/model/text/TextMethodItem.kt b/src/main/java/com/android/tools/metalava/model/text/TextMethodItem.kt
index 9dba79a..fcd4063 100644
--- a/src/main/java/com/android/tools/metalava/model/text/TextMethodItem.kt
+++ b/src/main/java/com/android/tools/metalava/model/text/TextMethodItem.kt
@@ -31,7 +31,7 @@
     name: String,
     containingClass: TextClassItem,
     modifiers: TextModifiers,
-    private val returnType: TextTypeItem?,
+    private val returnType: TextTypeItem,
     position: SourcePositionInfo
 ) : TextMemberItem(
     codebase, name, containingClass, position,
@@ -79,7 +79,7 @@
 
     override fun isConstructor(): Boolean = false
 
-    override fun returnType(): TypeItem? = returnType
+    override fun returnType(): TypeItem = returnType
 
     override fun superMethods(): List<MethodItem> {
         if (isConstructor()) {
@@ -209,9 +209,10 @@
     override var inheritedFrom: ClassItem? = null
 
     override fun toString(): String =
-        "${if (isConstructor()) "constructor" else "method"} ${containingClass().qualifiedName()}.${name()}(${parameters().joinToString {
-            it.type().toSimpleType()
-        }})"
+        "${if (isConstructor()) "constructor" else "method"} ${containingClass().qualifiedName()}.${toSignatureString()}"
+
+    fun toSignatureString(): String =
+        "${name()}(${parameters().joinToString { it.type().toSimpleType() }})"
 
     private var annotationDefault = ""
 
diff --git a/src/main/java/com/android/tools/metalava/model/text/TextModifiers.kt b/src/main/java/com/android/tools/metalava/model/text/TextModifiers.kt
index 2c72a0d..143ffcd 100644
--- a/src/main/java/com/android/tools/metalava/model/text/TextModifiers.kt
+++ b/src/main/java/com/android/tools/metalava/model/text/TextModifiers.kt
@@ -53,7 +53,7 @@
         annotationSources.forEach { source ->
             val index = source.indexOf('(')
             val originalName = if (index == -1) source.substring(1) else source.substring(1, index)
-            val qualifiedName = AnnotationItem.mapName(codebase, originalName)
+            val qualifiedName = AnnotationItem.mapName(originalName)
 
             // @Deprecated is also treated as a "modifier"
             if (qualifiedName == JAVA_LANG_DEPRECATED) {
diff --git a/src/main/java/com/android/tools/metalava/model/text/TextPackageItem.kt b/src/main/java/com/android/tools/metalava/model/text/TextPackageItem.kt
index d3bfc4d..7b126c3 100644
--- a/src/main/java/com/android/tools/metalava/model/text/TextPackageItem.kt
+++ b/src/main/java/com/android/tools/metalava/model/text/TextPackageItem.kt
@@ -31,10 +31,17 @@
 
     private val classes = ArrayList<TextClassItem>(100)
 
+    private val classesNames = HashSet<String>(100)
+
     fun name() = name
 
     fun addClass(classInfo: TextClassItem) {
+        val classFullName = classInfo.fullName()
+        if (classFullName in classesNames) {
+            return
+        }
         classes.add(classInfo)
+        classesNames.add(classFullName)
     }
 
     internal fun pruneClassList() {
diff --git a/src/main/java/com/android/tools/metalava/stub/JavaStubWriter.kt b/src/main/java/com/android/tools/metalava/stub/JavaStubWriter.kt
index 08d6d32..41e77ed 100644
--- a/src/main/java/com/android/tools/metalava/stub/JavaStubWriter.kt
+++ b/src/main/java/com/android/tools/metalava/stub/JavaStubWriter.kt
@@ -26,7 +26,6 @@
 import com.android.tools.metalava.model.ModifierList
 import com.android.tools.metalava.model.PackageItem
 import com.android.tools.metalava.model.TypeParameterList
-import com.android.tools.metalava.model.psi.PsiClassItem
 import com.android.tools.metalava.model.visitors.ItemVisitor
 import com.android.tools.metalava.options
 import java.io.PrintWriter
@@ -192,7 +191,6 @@
                     return
                 }
             }
-            (cls as PsiClassItem).psiClass.superClassType
             writer.print(qualifiedName)
         }
     }
@@ -355,7 +353,7 @@
 
         val returnType = method.returnType()
         writer.print(
-            returnType?.toTypeString(
+            returnType.toTypeString(
                 outerAnnotations = false,
                 innerAnnotations = generateAnnotations,
                 filter = filterReference
diff --git a/src/main/java/com/android/tools/metalava/stub/StubWriter.kt b/src/main/java/com/android/tools/metalava/stub/StubWriter.kt
index e11d8ef..2f27823 100644
--- a/src/main/java/com/android/tools/metalava/stub/StubWriter.kt
+++ b/src/main/java/com/android/tools/metalava/stub/StubWriter.kt
@@ -116,7 +116,9 @@
 
     private fun writePackageInfo(pkg: PackageItem) {
         val annotations = pkg.modifiers.annotations()
-        if (annotations.isNotEmpty() && generateAnnotations || !pkg.documentation.isBlank()) {
+        val writeAnnotations = annotations.isNotEmpty() && generateAnnotations
+        val writeDocumentation = docStubs && pkg.documentation.isNotBlank()
+        if (writeAnnotations || writeDocumentation) {
             val sourceFile = File(getPackageDir(pkg), "package-info.java")
             val packageInfoWriter = try {
                 PrintWriter(BufferedWriter(FileWriter(sourceFile)))
@@ -163,7 +165,10 @@
         assert(classItem.containingClass() == null) { "Should only be called on top level classes" }
         val packageDir = getPackageDir(classItem.containingPackage())
 
-        return if (classItem.isKotlin() && options.kotlinStubs) {
+        // Kotlin From-text stub generation is not supported.
+        // This method will raise an error if
+        // options.kotlinStubs == true and classItem is TextClassItem.
+        return if (options.kotlinStubs && classItem.isKotlin()) {
             File(packageDir, "${classItem.simpleName()}.kt")
         } else {
             File(packageDir, "${classItem.simpleName()}.java")
diff --git a/src/test/java/com/android/tools/metalava/ApiFileTest.kt b/src/test/java/com/android/tools/metalava/ApiFileTest.kt
index 31aae84..40e0ee7 100644
--- a/src/test/java/com/android/tools/metalava/ApiFileTest.kt
+++ b/src/test/java/com/android/tools/metalava/ApiFileTest.kt
@@ -2289,6 +2289,33 @@
     }
 
     @Test
+    fun `Check generic type signature insertion`() {
+        check(
+            format = FileFormat.V2,
+            sourceFiles = arrayOf(
+                java(
+                    """
+                    package test.pkg;
+                    public class MyClass {
+                        public <T> MyClass(Class<T> klass) { }
+                        public <U> void method1(Function<U> func) { }
+                    }
+                    """
+                )
+            ),
+            expectedIssues = "",
+            api = """
+                    package test.pkg {
+                      public class MyClass {
+                        ctor public <T> MyClass(Class<T>);
+                        method public <U> void method1(Function<U>);
+                      }
+                    }
+            """
+        )
+    }
+
+    @Test
     fun `When implementing rather than extending package private class, inline members instead`() {
         // If you implement a package private interface, we just remove it and inline the members into
         // the subclass
@@ -3777,7 +3804,7 @@
     }
 
     @Test
-    fun `Test cannot merging API signature files with duplicate class`() {
+    fun `Test can merge API signature files with duplicate class`() {
         val source1 = """
             package Test.pkg {
               public final class Class1 {
@@ -3792,9 +3819,79 @@
               }
             }
                     """
+        val expected = """
+            package Test.pkg {
+              public final class Class1 {
+                method public void method1();
+              }
+            }
+                    """
         check(
             signatureSources = arrayOf(source1, source2),
-            expectedFail = "Aborting: Unable to parse signature file: TESTROOT/project/load-api2.txt:2: Duplicate class found: Test.pkg.Class1"
+            api = expected
+        )
+    }
+
+    @Test
+    fun `Test can merge API signature files with generic type classes`() {
+        val source1 = """
+            package Test.pkg {
+              public class LinkedHashMap<K, V> extends java.util.HashMap<K,V> implements java.util.Map<K,V> {
+                ctor public LinkedHashMap(int, float);
+                ctor public LinkedHashMap(int);
+                ctor public LinkedHashMap();
+                ctor public LinkedHashMap(java.util.Map<? extends K,? extends V>);
+                ctor public LinkedHashMap(int, float, boolean);
+                method protected boolean removeEldestEntry(java.util.Map.Entry<K,V>);
+              }
+            }
+            """
+        val source2 = """
+            package Test.pkg {
+              public class LinkedHashMap<K, V> extends java.util.HashMap<K,V> implements java.util.Map<K,V> {
+                method public java.util.Map.Entry<K,V> eldest();
+              }
+            }
+            """
+        val expected = """
+            package Test.pkg {
+              public class LinkedHashMap<K, V> extends java.util.HashMap<K,V> implements java.util.Map<K,V> {
+                ctor public LinkedHashMap(int, float);
+                ctor public LinkedHashMap(int);
+                ctor public LinkedHashMap();
+                ctor public LinkedHashMap(java.util.Map<? extends K,? extends V>);
+                ctor public LinkedHashMap(int, float, boolean);
+                method public java.util.Map.Entry<K,V> eldest();
+                method protected boolean removeEldestEntry(java.util.Map.Entry<K,V>);
+              }
+            }
+            """
+        check(
+            signatureSources = arrayOf(source1, source2),
+            api = expected,
+            format = FileFormat.V2,
+        )
+    }
+
+    @Test
+    fun `Test cannot merge API signature files with incompatible class definitions`() {
+        val source1 = """
+            package Test.pkg {
+              public class Class1 {
+                method public void method2();
+              }
+            }
+                    """
+        val source2 = """
+            package Test.pkg {
+              public final class Class1 {
+                method public void method1();
+              }
+            }
+                    """
+        check(
+            signatureSources = arrayOf(source1, source2),
+            expectedFail = "Aborting: Unable to parse signature file: Incompatible class Test.pkg.Class1 definitions"
         )
     }
 
diff --git a/src/test/java/com/android/tools/metalava/ApiLintTest.kt b/src/test/java/com/android/tools/metalava/ApiLintTest.kt
index 2ac7c0a..8eec509 100644
--- a/src/test/java/com/android/tools/metalava/ApiLintTest.kt
+++ b/src/test/java/com/android/tools/metalava/ApiLintTest.kt
@@ -274,6 +274,10 @@
     @Test
     fun `Test callbacks`() {
         check(
+            extraArguments = arrayOf(
+                ARG_ERROR,
+                "CallbackInterface"
+            ),
             apiLint = "", // enabled
             expectedIssues = """
                 src/android/pkg/MyCallback.java:9: error: Callback method names must follow the on<Something> style: bar [CallbackMethodName] [See https://s.android.com/api-guidelines#callback-method-naming]
@@ -2274,6 +2278,13 @@
                             @NonNull
                             public UnitNameTestBuilder setOkPercentage(int i) { return this; }
                         }
+
+                        @NamedDataSpace
+                        public int getOkDataSpace() { return 0; }
+
+                        /** @hide */
+                        @android.annotation.IntDef
+                        public @interface NamedDataSpace {}
                     }
                     """
                 )
@@ -2286,8 +2297,8 @@
         check(
             apiLint = "", // enabled
             expectedIssues = """
-                src/android/pkg/MyErrorClass1.java:3: warning: Classes that release resources (close()) should implement AutoClosable and CloseGuard: class android.pkg.MyErrorClass1 [NotCloseable]
-                src/android/pkg/MyErrorClass2.java:3: warning: Classes that release resources (finalize(), shutdown()) should implement AutoClosable and CloseGuard: class android.pkg.MyErrorClass2 [NotCloseable]
+                src/android/pkg/MyErrorClass1.java:3: warning: Classes that release resources (close()) should implement AutoCloseable and CloseGuard: class android.pkg.MyErrorClass1 [NotCloseable]
+                src/android/pkg/MyErrorClass2.java:3: warning: Classes that release resources (finalize(), shutdown()) should implement AutoCloseable and CloseGuard: class android.pkg.MyErrorClass2 [NotCloseable]
                 """,
             sourceFiles = arrayOf(
                 java(
@@ -2346,8 +2357,8 @@
         check(
             apiLint = "", // enabled
             expectedIssues = """
-                src/android/pkg/MyErrorClass1.java:3: warning: Classes that release resources (close()) should implement AutoClosable and CloseGuard: class android.pkg.MyErrorClass1 [NotCloseable]
-                src/android/pkg/MyErrorClass2.java:3: warning: Classes that release resources (finalize(), shutdown()) should implement AutoClosable and CloseGuard: class android.pkg.MyErrorClass2 [NotCloseable]
+                src/android/pkg/MyErrorClass1.java:3: warning: Classes that release resources (close()) should implement AutoCloseable and CloseGuard: class android.pkg.MyErrorClass1 [NotCloseable]
+                src/android/pkg/MyErrorClass2.java:3: warning: Classes that release resources (finalize(), shutdown()) should implement AutoCloseable and CloseGuard: class android.pkg.MyErrorClass2 [NotCloseable]
             """,
             manifest = """<?xml version="1.0" encoding="UTF-8"?>
                 <manifest xmlns:android="http://schemas.android.com/apk/res/android">
diff --git a/src/test/java/com/android/tools/metalava/CompatibilityCheckTest.kt b/src/test/java/com/android/tools/metalava/CompatibilityCheckTest.kt
index 1dc846b..4a5c6d1 100644
--- a/src/test/java/com/android/tools/metalava/CompatibilityCheckTest.kt
+++ b/src/test/java/com/android/tools/metalava/CompatibilityCheckTest.kt
@@ -1200,6 +1200,7 @@
         check(
             expectedIssues = """
                 src/test/pkg/Outer.java:7: error: Method test.pkg.Outer.Class1.method1 has added 'final' qualifier [AddedFinal]
+                src/test/pkg/Outer.java:19: error: Method test.pkg.Outer.Class4.method4 has removed 'final' qualifier [RemovedFinal]
                 """,
             checkCompatibilityApiReleased = """
                 package test.pkg {
@@ -1776,6 +1777,94 @@
     }
 
     @Test
+    fun `Incompatible Changes in Released System API `() {
+        // Incompatible changes to a released System API should be detected
+        // In this case removing final and changing value of constant
+        check(
+            includeSystemApiAnnotations = true,
+            expectedIssues = """
+                src/android/rolecontrollerservice/RoleControllerService.java:8: error: Method android.rolecontrollerservice.RoleControllerService.sendNetworkScore has removed 'final' qualifier [RemovedFinal]
+                src/android/rolecontrollerservice/RoleControllerService.java:9: error: Field android.rolecontrollerservice.RoleControllerService.APP_RETURN_UNWANTED has changed value from 1 to 0 [ChangedValue]
+                """,
+            sourceFiles = arrayOf(
+                java(
+                    """
+                    package android.rolecontrollerservice;
+                    import android.annotation.SystemApi;
+
+                    /** @hide */
+                    @SystemApi
+                    public abstract class RoleControllerService {
+                        public abstract void onGrantDefaultRoles();
+                        public void sendNetworkScore();
+                        public static final int APP_RETURN_UNWANTED = 0;
+                    }
+                    """
+                ),
+                systemApiSource
+            ),
+
+            extraArguments = arrayOf(
+                ARG_SHOW_ANNOTATION, "android.annotation.TestApi",
+                ARG_HIDE_PACKAGE, "android.annotation",
+            ),
+
+            checkCompatibilityApiReleased =
+            """
+                package android.rolecontrollerservice {
+                  public abstract class RoleControllerService {
+                    ctor public RoleControllerService();
+                    method public abstract void onGrantDefaultRoles();
+                    method public final void sendNetworkScore();
+                    field public static final int APP_RETURN_UNWANTED = 1;
+                  }
+                }
+                """
+        )
+    }
+
+    @Test
+    fun `Incompatible changes to released API signature codebase`() {
+        // Incompatible changes to a released System API should be detected
+        // in case of partial files
+        check(
+            expectedIssues = """
+                TESTROOT/released-api.txt:5: error: Removed method test.pkg.Foo.method2() [RemovedMethod]
+                """,
+            signatureSource = """
+                // Signature format: 3.0
+                package test.pkg {
+                  public final class Foo {
+                    ctor public Foo();
+                    method public void method1();
+                  }
+                }
+                """,
+
+            checkCompatibilityApiReleased =
+            """
+                package test.pkg {
+                  public final class Foo {
+                    ctor public Foo();
+                    method public void method1();
+                    method public void method2();
+                    method public void method3();
+                  }
+                }
+                """,
+            checkCompatibilityBaseApi =
+            """
+                package test.pkg {
+                  public final class Foo {
+                    ctor public Foo();
+                    method public void method3();
+                  }
+                }
+                """,
+        )
+    }
+
+    @Test
     fun `Partial text file which adds methods to show-annotation API`() {
         // This happens in system and test files where we only include APIs that differ
         // from the base IDE. When parsing these code bases we need to gracefully handle
@@ -3266,7 +3355,7 @@
         // Regression test for 130567941
         check(
             expectedIssues = """
-            TESTROOT/load-api.txt:7: error: Method test.pkg.sample.SampleClass.convert has changed return type from Number to java.lang.Number [ChangedType]
+            TESTROOT/load-api.txt:7: error: Method test.pkg.sample.SampleClass.convert1 has changed return type from Number to java.lang.Number [ChangedType]
             """,
             inputKotlinStyleNulls = true,
             outputKotlinStyleNulls = true,
@@ -3275,7 +3364,7 @@
                 package test.pkg.sample {
                   public abstract class SampleClass {
                     method public <Number> Number! convert(Number);
-                    method public <Number> Number! convert(Number);
+                    method public <Number> Number! convert1(Number);
                   }
                 }
                 """,
@@ -3286,7 +3375,7 @@
                     // Here the generic type parameter applies to both the function argument and the function return type
                     method public <Number> Number! convert(Number);
                     // Here the generic type parameter applies to the function argument but not the function return type
-                    method public <Number> java.lang.Number! convert(Number);
+                    method public <Number> java.lang.Number! convert1(Number);
                   }
                 }
             """
diff --git a/src/test/java/com/android/tools/metalava/CoreApiTest.kt b/src/test/java/com/android/tools/metalava/CoreApiTest.kt
index b3429be..6147db7 100644
--- a/src/test/java/com/android/tools/metalava/CoreApiTest.kt
+++ b/src/test/java/com/android/tools/metalava/CoreApiTest.kt
@@ -209,7 +209,8 @@
             extraArguments = arrayOf(
                 ARG_SHOW_SINGLE_ANNOTATION, "libcore.api.IntraCoreApi",
                 ARG_HIDE_ANNOTATION, "libcore.api.LibCoreHidden"
-            )
+            ),
+            docStubs = true
         )
     }
 
diff --git a/src/test/java/com/android/tools/metalava/DocAnalyzerTest.kt b/src/test/java/com/android/tools/metalava/DocAnalyzerTest.kt
index de91122..71d52a4 100644
--- a/src/test/java/com/android/tools/metalava/DocAnalyzerTest.kt
+++ b/src/test/java/com/android/tools/metalava/DocAnalyzerTest.kt
@@ -238,34 +238,27 @@
                     /**
                      * Requires {@link android.Manifest.permission#ACCESS_COARSE_LOCATION}
                      */
-                    @androidx.annotation.RequiresPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION)
                     public void test1() { throw new RuntimeException("Stub!"); }
                     /**
                      * Requires {@link android.Manifest.permission#ACCESS_COARSE_LOCATION}
                      */
-                    @androidx.annotation.RequiresPermission(allOf=android.Manifest.permission.ACCESS_COARSE_LOCATION)
                     public void test2() { throw new RuntimeException("Stub!"); }
                     /**
                      * Requires {@link android.Manifest.permission#ACCESS_COARSE_LOCATION} or {@link android.Manifest.permission#ACCESS_FINE_LOCATION}
                      */
-                    @androidx.annotation.RequiresPermission(anyOf={android.Manifest.permission.ACCESS_COARSE_LOCATION, android.Manifest.permission.ACCESS_FINE_LOCATION})
                     public void test3() { throw new RuntimeException("Stub!"); }
                     /**
                      * Requires {@link android.Manifest.permission#ACCESS_COARSE_LOCATION} and {@link android.Manifest.permission#ACCOUNT_MANAGER}
                      */
-                    @androidx.annotation.RequiresPermission(allOf={android.Manifest.permission.ACCESS_COARSE_LOCATION, android.Manifest.permission.ACCOUNT_MANAGER})
                     public void test4() { throw new RuntimeException("Stub!"); }
-                    @androidx.annotation.RequiresPermission(value=android.Manifest.permission.WATCH_APPOPS, conditional=true)
                     public void test5() { throw new RuntimeException("Stub!"); }
                     /**
                      * Requires {@link android.Manifest.permission#ACCESS_COARSE_LOCATION} or {@link android.telephony.TelephonyManager#hasCarrierPrivileges carrier privileges}
                      */
-                    @androidx.annotation.RequiresPermission(anyOf={android.Manifest.permission.ACCESS_COARSE_LOCATION, "carrier privileges"})
                     public void test6() { throw new RuntimeException("Stub!"); }
                     /**
                      * Requires {@link android.Manifest.permission#ACCESS_COARSE_LOCATION} or "carier priviliges"
                      */
-                    @androidx.annotation.RequiresPermission(anyOf={android.Manifest.permission.ACCESS_COARSE_LOCATION, "carier priviliges"})
                     public void test6() { throw new RuntimeException("Stub!"); }
                     }
                     """
@@ -315,7 +308,6 @@
                     @SuppressWarnings({"unchecked", "deprecation", "all"})
                     public class PermissionTest {
                     public PermissionTest() { throw new RuntimeException("Stub!"); }
-                    @androidx.annotation.RequiresPermission(value=android.Manifest.permission.WATCH_APPOPS, conditional=true)
                     public void test1() { throw new RuntimeException("Stub!"); }
                     }
                     """
@@ -361,17 +353,14 @@
                      * @param range2 Value is 20 or greater
                      * @return Value is 10 or greater
                      */
-                    @androidx.annotation.IntRange(from=10)
-                    public int test1(@androidx.annotation.IntRange(from=20) int range2) { throw new RuntimeException("Stub!"); }
+                    public int test1(int range2) { throw new RuntimeException("Stub!"); }
                     /**
                      * @return Value is between 10 and 20 inclusive
                      */
-                    @androidx.annotation.IntRange(from=10, to=20)
                     public int test2() { throw new RuntimeException("Stub!"); }
                     /**
                      * @return Value is 100 or less
                      */
-                    @androidx.annotation.IntRange(to=100)
                     public int test3() { throw new RuntimeException("Stub!"); }
                     }
                     """
@@ -411,14 +400,12 @@
                      * main thread of your app. *
                      */
                     @SuppressWarnings({"unchecked", "deprecation", "all"})
-                    @androidx.annotation.UiThread
                     public class RangeTest {
                     public RangeTest() { throw new RuntimeException("Stub!"); }
                     /**
                      * This method may take several seconds to complete, so it should
                      * only be called from a worker thread.
                      */
-                    @androidx.annotation.WorkerThread
                     public int test1() { throw new RuntimeException("Stub!"); }
                     }
                     """
@@ -462,8 +449,6 @@
                      * This method may take several seconds to complete, so it should
                      * only be called from a worker thread.
                      */
-                    @androidx.annotation.UiThread
-                    @androidx.annotation.WorkerThread
                     public int test1() { throw new RuntimeException("Stub!"); }
                     }
                     """
@@ -536,7 +521,6 @@
                      * @return blah blah blah
                      * @apiSince 24
                      */
-                    @androidx.annotation.UiThread
                     public int getCurrentContentInsetEnd() { throw new RuntimeException("Stub!"); }
                     /**
                      * <br>
@@ -544,7 +528,6 @@
                      * this UI element. This is typically the main thread of your app.
                      * @apiSince 15
                      */
-                    @androidx.annotation.UiThread
                     public int getCurrentContentInsetRight() { throw new RuntimeException("Stub!"); }
                     }
                     """
@@ -712,7 +695,6 @@
                     /**
                      * Requires {@link test.pkg.RangeTest#ACCESS_COARSE_LOCATION}
                      */
-                    @androidx.annotation.RequiresPermission(test.pkg.RangeTest.ACCESS_COARSE_LOCATION)
                     public void test1() { throw new RuntimeException("Stub!"); }
                     public static final java.lang.String ACCESS_COARSE_LOCATION = "android.permission.ACCESS_COARSE_LOCATION";
                     }
@@ -752,7 +734,6 @@
                     /**
                      * Requires "MyPermission"
                      */
-                    @androidx.annotation.RequiresPermission("MyPermission")
                     public void test1() { throw new RuntimeException("Stub!"); }
                     }
                     """
@@ -794,7 +775,6 @@
                      * <br>
                      * Requires {@link test.pkg.RangeTest#ACCESS_COARSE_LOCATION}
                      */
-                    @androidx.annotation.RequiresPermission(test.pkg.RangeTest.ACCESS_COARSE_LOCATION)
                     public int test1() { throw new RuntimeException("Stub!"); }
                     public static final java.lang.String ACCESS_COARSE_LOCATION = "android.permission.ACCESS_COARSE_LOCATION";
                     }
@@ -841,7 +821,6 @@
                      * <br>
                      * Requires {@link test.pkg.RangeTest#ACCESS_COARSE_LOCATION}
                      */
-                    @androidx.annotation.RequiresPermission(test.pkg.RangeTest.ACCESS_COARSE_LOCATION)
                     public int test1() { throw new RuntimeException("Stub!"); }
                     public static final java.lang.String ACCESS_COARSE_LOCATION = "android.permission.ACCESS_COARSE_LOCATION";
                     }
@@ -878,7 +857,7 @@
                     /**
                      * @param parameter2 Value is 10 or greater
                      */
-                    public int test1(int parameter1, @androidx.annotation.IntRange(from=10) int parameter2, int parameter3) { throw new RuntimeException("Stub!"); }
+                    public int test1(int parameter1, int parameter2, int parameter3) { throw new RuntimeException("Stub!"); }
                     }
                     """
                 )
@@ -929,7 +908,6 @@
                      * @param parameter3 docs for parameter2
                      * @return return value documented here
                      */
-                    @androidx.annotation.RequiresPermission(test.pkg.RangeTest.ACCESS_COARSE_LOCATION)
                     public int test1(int parameter1, int parameter2, int parameter3) { throw new RuntimeException("Stub!"); }
                     public static final java.lang.String ACCESS_COARSE_LOCATION = "android.permission.ACCESS_COARSE_LOCATION";
                     }
@@ -972,7 +950,7 @@
                      * @param parameter2 Value is 10 or greater
                      * @return return value documented here
                      */
-                    public int test1(int parameter1, @androidx.annotation.IntRange(from=10) int parameter2, int parameter3) { throw new RuntimeException("Stub!"); }
+                    public int test1(int parameter1, int parameter2, int parameter3) { throw new RuntimeException("Stub!"); }
                     }
                     """
                 )
@@ -1017,7 +995,7 @@
                      * @param parameter2 Value is 10 or greater
                      * @return return value documented here
                      */
-                    public int test1(int parameter1, @androidx.annotation.IntRange(from=10) int parameter2, int parameter3) { throw new RuntimeException("Stub!"); }
+                    public int test1(int parameter1, int parameter2, int parameter3) { throw new RuntimeException("Stub!"); }
                     }
                     """
                 )
@@ -1064,7 +1042,7 @@
                      * @param parameter3 docs for parameter2
                      * @return return value documented here
                      */
-                    public int test1(int parameter1, @androidx.annotation.IntRange(from=10) int parameter2, int parameter3) { throw new RuntimeException("Stub!"); }
+                    public int test1(int parameter1, int parameter2, int parameter3) { throw new RuntimeException("Stub!"); }
                     }
                     """
                 )
@@ -1100,7 +1078,6 @@
                     /**
                      * @return Value is 10 or greater
                      */
-                    @androidx.annotation.IntRange(from=10)
                     public int test1(int parameter1, int parameter2, int parameter3) { throw new RuntimeException("Stub!"); }
                     }
                     """
@@ -1143,7 +1120,6 @@
                      * @return return value documented here
                      * Value is 10 or greater
                      */
-                    @androidx.annotation.IntRange(from=10)
                     public int test1(int parameter1, int parameter2, int parameter3) { throw new RuntimeException("Stub!"); }
                     }
                     """
@@ -1517,6 +1493,188 @@
         )
     }
 
+    object SdkExtSinceConstants {
+        val sourceFiles = arrayOf(
+            java(
+                """
+                package android.pkg;
+                public class Test {
+                   public static final String UNIT_TEST_1 = "unit.test.1";
+                   public static final String UNIT_TEST_2 = "unit.test.2";
+                   public static final String UNIT_TEST_3 = "unit.test.3";
+                   public Test() {}
+                   public void foo() {}
+                   public class Inner {
+                       public Inner() {}
+                       public static final boolean UNIT_TEST_4 = true;
+                   }
+                }
+                """
+            )
+        )
+
+        const val apiVersionsXml = """
+                <?xml version="1.0" encoding="utf-8"?>
+                <api version="3">
+                    <sdk id="30" shortname="R-ext" name="R Extensions" reference="android/os/Build${'$'}VERSION_CODES${'$'}R" />
+                    <sdk id="31" shortname="S-ext" name="S Extensions" reference="android/os/Build${'$'}VERSION_CODES${'$'}S" />
+                    <sdk id="33" shortname="T-ext" name="T Extensions" reference="android/os/Build${'$'}VERSION_CODES${'$'}T" />
+                    <sdk id="1000000" shortname="standalone-ext" name="Standalone Extensions" reference="some/other/CONST" />
+                    <class name="android/pkg/Test" since="1" sdks="0:1,30:2,31:2,33:2">
+                        <method name="foo()V"/>
+                        <method name="&lt;init>()V"/>
+                        <field name="UNIT_TEST_1"/>
+                        <field name="UNIT_TEST_2" since="2" sdks="1000000:3,31:3,33:3,0:2"/>
+                        <field name="UNIT_TEST_3" since="31" sdks="1000000:4"/>
+                    </class>
+                    <class name="android/pkg/Test${'$'}Inner" since="1" sdks="0:1,30:2,31:2,33:2">
+                        <method name="&lt;init>()V"/>
+                        <field name="UNIT_TEST_4"/>
+                    </class>
+                </api>
+                """
+
+        const val docStubsSourceList = """
+                TESTROOT/stubs/android/pkg/package-info.java
+                TESTROOT/stubs/android/pkg/Test.java
+            """
+    }
+
+    @Test
+    fun `@sdkExtSince (finalized, no codename)`() {
+        check(
+            extraArguments = arrayOf(
+                ARG_CURRENT_VERSION,
+                "30",
+            ),
+            sourceFiles = SdkExtSinceConstants.sourceFiles,
+            applyApiLevelsXml = SdkExtSinceConstants.apiVersionsXml,
+            checkCompilation = true,
+            docStubs = true,
+            docStubsSourceList = SdkExtSinceConstants.docStubsSourceList,
+            stubFiles = arrayOf(
+                java(
+                    """
+                    package android.pkg;
+                    /**
+                     * @apiSince 1
+                     * @sdkExtSince R Extensions 2
+                     */
+                    @SuppressWarnings({"unchecked", "deprecation", "all"})
+                    public class Test {
+                    /**
+                     * @apiSince 1
+                     * @sdkExtSince R Extensions 2
+                     */
+                    public Test() { throw new RuntimeException("Stub!"); }
+                    /**
+                     * @apiSince 1
+                     * @sdkExtSince R Extensions 2
+                     */
+                    public void foo() { throw new RuntimeException("Stub!"); }
+                    /**
+                     * @apiSince 1
+                     * @sdkExtSince R Extensions 2
+                     */
+                    public static final java.lang.String UNIT_TEST_1 = "unit.test.1";
+                    /**
+                     * @apiSince 2
+                     * @sdkExtSince Standalone Extensions 3
+                     */
+                    public static final java.lang.String UNIT_TEST_2 = "unit.test.2";
+                    /** @sdkExtSince Standalone Extensions 4 */
+                    public static final java.lang.String UNIT_TEST_3 = "unit.test.3";
+                    /**
+                     * @apiSince 1
+                     * @sdkExtSince R Extensions 2
+                     */
+                    @SuppressWarnings({"unchecked", "deprecation", "all"})
+                    public class Inner {
+                    /**
+                     * @apiSince 1
+                     * @sdkExtSince R Extensions 2
+                     */
+                    public Inner() { throw new RuntimeException("Stub!"); }
+                    /**
+                     * @apiSince 1
+                     * @sdkExtSince R Extensions 2
+                     */
+                    public static final boolean UNIT_TEST_4 = true;
+                    }
+                    }
+                    """
+                )
+            )
+        )
+    }
+
+    @Test
+    fun `@sdkExtSince (not finalized)`() {
+        check(
+            sourceFiles = SdkExtSinceConstants.sourceFiles,
+            applyApiLevelsXml = SdkExtSinceConstants.apiVersionsXml,
+            checkCompilation = true,
+            docStubs = true,
+            docStubsSourceList = SdkExtSinceConstants.docStubsSourceList,
+            stubFiles = arrayOf(
+                java(
+                    """
+                    package android.pkg;
+                    /**
+                     * @apiSince 1
+                     * @sdkExtSince R Extensions 2
+                     */
+                    @SuppressWarnings({"unchecked", "deprecation", "all"})
+                    public class Test {
+                    /**
+                     * @apiSince 1
+                     * @sdkExtSince R Extensions 2
+                     */
+                    public Test() { throw new RuntimeException("Stub!"); }
+                    /**
+                     * @apiSince 1
+                     * @sdkExtSince R Extensions 2
+                     */
+                    public void foo() { throw new RuntimeException("Stub!"); }
+                    /**
+                     * @apiSince 1
+                     * @sdkExtSince R Extensions 2
+                     */
+                    public static final java.lang.String UNIT_TEST_1 = "unit.test.1";
+                    /**
+                     * @apiSince 2
+                     * @sdkExtSince Standalone Extensions 3
+                     */
+                    public static final java.lang.String UNIT_TEST_2 = "unit.test.2";
+                    /**
+                     * @apiSince 31
+                     * @sdkExtSince Standalone Extensions 4
+                     */
+                    public static final java.lang.String UNIT_TEST_3 = "unit.test.3";
+                    /**
+                     * @apiSince 1
+                     * @sdkExtSince R Extensions 2
+                     */
+                    @SuppressWarnings({"unchecked", "deprecation", "all"})
+                    public class Inner {
+                    /**
+                     * @apiSince 1
+                     * @sdkExtSince R Extensions 2
+                     */
+                    public Inner() { throw new RuntimeException("Stub!"); }
+                    /**
+                     * @apiSince 1
+                     * @sdkExtSince R Extensions 2
+                     */
+                    public static final boolean UNIT_TEST_4 = true;
+                    }
+                    }
+                    """
+                )
+            )
+        )
+    }
+
     @Test
     fun `Generate overview html docs`() {
         // If a codebase provides overview.html files in the a public package,
@@ -1707,7 +1865,6 @@
                     package test.pkg;
                     /** @apiSince 21 */
                     @SuppressWarnings({"unchecked", "deprecation", "all"})
-                    @androidx.annotation.RequiresApi(21)
                     public class MyClass1 {
                     public MyClass1() { throw new RuntimeException("Stub!"); }
                     }
diff --git a/src/test/java/com/android/tools/metalava/DriverTest.kt b/src/test/java/com/android/tools/metalava/DriverTest.kt
index b8f2f85..e56e041 100644
--- a/src/test/java/com/android/tools/metalava/DriverTest.kt
+++ b/src/test/java/com/android/tools/metalava/DriverTest.kt
@@ -77,7 +77,7 @@
     }
 
     protected fun createProject(vararg files: TestFile): File {
-        val dir = temporaryFolder.newFolder("project")
+        val dir = newFolder("project")
 
         files
             .map { it.createFile(dir) }
@@ -86,11 +86,33 @@
         return dir
     }
 
+    private fun newFolder(children: String = ""): File {
+        var dir = File(temporaryFolder.root.path, children)
+        return if (dir.exists()) {
+            dir
+        } else {
+            temporaryFolder.newFolder(children)
+        }
+    }
+
+    private fun newFile(children: String = ""): File {
+        var dir = File(temporaryFolder.root.path, children)
+        return if (dir.exists()) {
+            dir
+        } else {
+            temporaryFolder.newFile(children)
+        }
+    }
+
     // Makes a note to fail the test, but still allows the test to complete before failing
     protected fun addError(error: String) {
         errorCollector.addError(Throwable(error))
     }
 
+    protected fun getApiFile(): File {
+        return File(temporaryFolder.root.path, "public-api.txt")
+    }
+
     protected fun runDriver(vararg args: String, expectedFail: String = ""): String {
 
         resetTicker()
@@ -119,7 +141,7 @@
                     actualFail.replace(".", "").trim()
                 ) {
                     val reportedCompatError = actualFail.startsWith("Aborting: Found compatibility problems checking the ")
-                    if (expectedFail == "Aborting: Found compatibility problems with --check-compatibility" &&
+                    if (expectedFail == "Aborting: Found compatibility problems" &&
                         reportedCompatError
                     ) {
                         // Special case for compat checks; we don't want to force each one of them
@@ -347,11 +369,6 @@
         /** Enable nullability validation for the listed classes */
         validateNullabilityFromList: String? = null,
         /**
-         * Whether to include source retention annotations in the stubs (in that case they do not
-         * go into the extracted annotations zip file)
-         */
-        includeSourceRetentionAnnotations: Boolean = true,
-        /**
          * Whether to include the signature version in signatures
          */
         includeSignatureVersion: Boolean = false,
@@ -416,7 +433,7 @@
             expectedFail != null -> expectedFail
             (checkCompatibilityApiReleased != null || checkCompatibilityRemovedApiReleased != null) &&
                 expectedIssues != null && expectedIssues.trim().isNotEmpty() -> {
-                "Aborting: Found compatibility problems with --check-compatibility"
+                "Aborting: Found compatibility problems"
             }
             else -> ""
         }
@@ -703,13 +720,6 @@
                 emptyArray()
             }
 
-        val includeSourceRetentionAnnotationArgs =
-            if (includeSourceRetentionAnnotations) {
-                arrayOf(ARG_INCLUDE_SOURCE_RETENTION)
-            } else {
-                emptyArray()
-            }
-
         var removedApiFile: File? = null
         val removedArgs = if (removedApi != null) {
             removedApiFile = temporaryFolder.newFile("removed.txt")
@@ -718,13 +728,9 @@
             emptyArray()
         }
 
-        var apiFile: File? = null
-        val apiArgs = if (api != null) {
-            apiFile = temporaryFolder.newFile("public-api.txt")
-            arrayOf(ARG_API, apiFile.path)
-        } else {
-            emptyArray()
-        }
+        // Always pass apiArgs and generate API text file in runDriver
+        var apiFile: File = newFile("public-api.txt")
+        val apiArgs = arrayOf(ARG_API, apiFile.path)
 
         var apiXmlFile: File? = null
         val apiXmlArgs = if (apiXml != null) {
@@ -796,7 +802,7 @@
 
         var stubsDir: File? = null
         val stubsArgs = if (stubFiles.isNotEmpty()) {
-            stubsDir = temporaryFolder.newFolder("stubs")
+            stubsDir = newFolder("stubs")
             if (docStubs) {
                 arrayOf(ARG_DOC_STUBS, stubsDir.path)
             } else {
@@ -928,12 +934,12 @@
             validateNullabilityTxt = null
             emptyArray()
         }
-        val validateNullablityFromListFile: File?
+        val validateNullabilityFromListFile: File?
         val validateNullabilityFromListArgs = if (validateNullabilityFromList != null) {
-            validateNullablityFromListFile = temporaryFolder.newFile("validate-nullability-classes.txt")
-            validateNullablityFromListFile.writeText(validateNullabilityFromList)
+            validateNullabilityFromListFile = temporaryFolder.newFile("validate-nullability-classes.txt")
+            validateNullabilityFromListFile.writeText(validateNullabilityFromList)
             arrayOf(
-                ARG_VALIDATE_NULLABILITY_FROM_LIST, validateNullablityFromListFile.path
+                ARG_VALIDATE_NULLABILITY_FROM_LIST, validateNullabilityFromListFile.path
             )
         } else {
             emptyArray()
@@ -963,7 +969,7 @@
             // test root folder such that we clean up the output strings referencing
             // paths to the temp folder
             "--temp-folder",
-            temporaryFolder.newFolder("temp").path,
+            newFolder("temp").path,
 
             // Annotation generation temporarily turned off by default while integrating with
             // SDK builds; tests need these
@@ -1007,7 +1013,6 @@
             *hideMetaAnnotationArguments,
             *showForStubPurposesAnnotationArguments,
             *showUnannotatedArgs,
-            *includeSourceRetentionAnnotationArgs,
             *apiLintArgs,
             *sdkFilesArgs,
             *importedPackageArgs.toTypedArray(),
@@ -1041,7 +1046,7 @@
             assertEquals(expectedOutput.trimIndent().trim(), actualOutput.trim())
         }
 
-        if (api != null && apiFile != null) {
+        if (api != null) {
             assertTrue("${apiFile.path} does not exist even though --api was used", apiFile.exists())
             val actualText = readFile(apiFile, stripBlankLines, trim)
             assertEquals(prepareExpectedApi(api, format), actualText)
@@ -1192,15 +1197,21 @@
         if (stubFiles.isNotEmpty()) {
             for (expected in stubFiles) {
                 val actual = File(stubsDir!!, expected.targetRelativePath)
-                if (actual.exists()) {
-                    val actualContents = readFile(actual, stripBlankLines, trim)
-                    assertEquals(expected.contents, actualContents)
-                } else {
-                    val existing = stubsDir.walkTopDown().filter { it.isFile }.map { it.path }.joinToString("\n  ")
+                if (!actual.exists()) {
+                    val existing = stubsDir.walkTopDown()
+                        .filter { it.isFile }
+                        .map { it.path }
+                        .joinToString("\n  ")
                     throw FileNotFoundException(
-                        "Could not find a generated stub for ${expected.targetRelativePath}. Found these files: \n  $existing"
+                        "Could not find a generated stub for ${expected.targetRelativePath}. " +
+                            "Found these files: \n  $existing"
                     )
                 }
+                val actualContents = readFile(actual, stripBlankLines, trim)
+                val stubSource = if (sourceFiles.isEmpty()) "text" else "source"
+                val message =
+                    "Generated from-$stubSource stub contents does not match expected contents"
+                assertEquals(message, expected.contents, actualContents)
             }
         }
 
@@ -1364,7 +1375,8 @@
             }
         }
 
-        private fun readFile(file: File, stripBlankLines: Boolean = false, trim: Boolean = false): String {
+        @JvmStatic
+        protected fun readFile(file: File, stripBlankLines: Boolean = false, trim: Boolean = false): String {
             var apiLines: List<String> = Files.asCharSource(file, UTF_8).readLines()
             if (stripBlankLines) {
                 apiLines = apiLines.asSequence().filter { it.isNotBlank() }.toList()
diff --git a/src/test/java/com/android/tools/metalava/ExtractAnnotationsTest.kt b/src/test/java/com/android/tools/metalava/ExtractAnnotationsTest.kt
index 0152c82..0a8b802 100644
--- a/src/test/java/com/android/tools/metalava/ExtractAnnotationsTest.kt
+++ b/src/test/java/com/android/tools/metalava/ExtractAnnotationsTest.kt
@@ -74,7 +74,6 @@
     @Test
     fun `Check java typedef extraction and warning about non-source retention of typedefs`() {
         check(
-            includeSourceRetentionAnnotations = false,
             format = FileFormat.V2,
             sourceFiles = sourceFiles1,
             expectedIssues = "src/test/pkg/IntDefTest.java:13: error: This typedef annotation class should have @Retention(RetentionPolicy.SOURCE) [AnnotationExtraction]",
@@ -108,7 +107,6 @@
     @Test
     fun `Check Kotlin and referencing hidden constants from typedef`() {
         check(
-            includeSourceRetentionAnnotations = false,
             sourceFiles = arrayOf(
                 kotlin(
                     """
@@ -186,7 +184,6 @@
     @Test
     fun `Check including only class retention annotations other than typedefs`() {
         check(
-            includeSourceRetentionAnnotations = true,
             sourceFiles = arrayOf(
                 kotlin(
                     """
@@ -264,7 +261,6 @@
     @Test
     fun `Extract permission annotations`() {
         check(
-            includeSourceRetentionAnnotations = false,
             sourceFiles = arrayOf(
                 java(
                     """
@@ -333,7 +329,6 @@
     @Test
     fun `Include merged annotations in exported source annotations`() {
         check(
-            includeSourceRetentionAnnotations = true,
             outputKotlinStyleNulls = false,
             includeSystemApiAnnotations = false,
             expectedIssues = "error: Unexpected reference to Nonexistent.Field [InternalError]",
@@ -397,7 +392,6 @@
     @Test
     fun `Only including class retention annotations in stubs`() {
         check(
-            includeSourceRetentionAnnotations = false,
             outputKotlinStyleNulls = false,
             includeSystemApiAnnotations = false,
             sourceFiles = arrayOf(
@@ -446,7 +440,6 @@
     @Test
     fun `Check warning about unexpected returns from typedef method`() {
         check(
-            includeSourceRetentionAnnotations = false,
             expectedIssues = "src/test/pkg/IntDefTest.java:36: warning: Returning unexpected constant UNRELATED; is @DialogStyle missing this constant? Expected one of STYLE_NORMAL, STYLE_NO_TITLE, STYLE_NO_FRAME, STYLE_NO_INPUT [ReturningUnexpectedConstant]",
             sourceFiles = arrayOf(
                 java(
@@ -523,7 +516,6 @@
     @Test
     fun `No typedef signatures in api files`() {
         check(
-            includeSourceRetentionAnnotations = false,
             extraArguments = arrayOf(
                 ARG_HIDE_PACKAGE, "android.annotation",
                 ARG_TYPEDEFS_IN_SIGNATURES, "none"
@@ -559,7 +551,6 @@
     @Test
     fun `Inlining typedef signatures in api files`() {
         check(
-            includeSourceRetentionAnnotations = false,
             extraArguments = arrayOf(
                 ARG_HIDE_PACKAGE, "android.annotation",
                 ARG_TYPEDEFS_IN_SIGNATURES, "inline"
@@ -595,7 +586,6 @@
     @Test
     fun `Referencing typedef signatures in api files`() {
         check(
-            includeSourceRetentionAnnotations = false,
             extraArguments = arrayOf(
                 ARG_HIDE_PACKAGE, "android.annotation",
                 ARG_TYPEDEFS_IN_SIGNATURES, "ref"
@@ -631,7 +621,6 @@
     @Test
     fun `Test generics in XML attributes are encoded`() {
         check(
-            includeSourceRetentionAnnotations = false,
             outputKotlinStyleNulls = false,
             includeSystemApiAnnotations = false,
             sourceFiles = arrayOf(
diff --git a/src/test/java/com/android/tools/metalava/NullnessMigrationTest.kt b/src/test/java/com/android/tools/metalava/NullnessMigrationTest.kt
index 95920c3..4565002 100644
--- a/src/test/java/com/android/tools/metalava/NullnessMigrationTest.kt
+++ b/src/test/java/com/android/tools/metalava/NullnessMigrationTest.kt
@@ -98,7 +98,7 @@
                     package test.pkg;
                     @SuppressWarnings({"unchecked", "deprecation", "all"})
                     public abstract class MyTest {
-                    private MyTest() { throw new RuntimeException("Stub!"); }
+                    MyTest() { throw new RuntimeException("Stub!"); }
                     @androidx.annotation.RecentlyNullable
                     public java.lang.Double convert1(java.lang.Float f) { throw new RuntimeException("Stub!"); }
                     }
@@ -148,7 +148,7 @@
                     package test.pkg;
                     @SuppressWarnings({"unchecked", "deprecation", "all"})
                     public abstract class MyTest {
-                    private MyTest() { throw new RuntimeException("Stub!"); }
+                    MyTest() { throw new RuntimeException("Stub!"); }
                     public java.lang.Double convert1(@androidx.annotation.RecentlyNonNull java.lang.Float f) { throw new RuntimeException("Stub!"); }
                     }
                     """
@@ -815,7 +815,7 @@
                     package test.pkg;
                     @SuppressWarnings({"unchecked", "deprecation", "all"})
                     public class Child1 extends test.pkg.Parent {
-                    private Child1() { throw new RuntimeException("Stub!"); }
+                    Child1() { throw new RuntimeException("Stub!"); }
                     public void method1(@androidx.annotation.RecentlyNonNull java.lang.String first, int second) { throw new RuntimeException("Stub!"); }
                     }
                     """
@@ -825,7 +825,7 @@
                     package test.pkg;
                     @SuppressWarnings({"unchecked", "deprecation", "all"})
                     public class Child2 extends test.pkg.Parent {
-                    private Child2() { throw new RuntimeException("Stub!"); }
+                    Child2() { throw new RuntimeException("Stub!"); }
                     public void method0(java.lang.String first, int second) { throw new RuntimeException("Stub!"); }
                     public void method1(java.lang.String first, int second) { throw new RuntimeException("Stub!"); }
                     public void method2(@androidx.annotation.RecentlyNonNull java.lang.String first, int second) { throw new RuntimeException("Stub!"); }
diff --git a/src/test/java/com/android/tools/metalava/OptionsTest.kt b/src/test/java/com/android/tools/metalava/OptionsTest.kt
index 4390830..47e437a 100644
--- a/src/test/java/com/android/tools/metalava/OptionsTest.kt
+++ b/src/test/java/com/android/tools/metalava/OptionsTest.kt
@@ -241,13 +241,9 @@
                                              encoded its types using Kotlin style types: a suffix of "?" for nullable
                                              types, no suffix for non nullable types, and "!" for unknown. The default
                                              is no.
---check-compatibility:type:state <file>
+--check-compatibility:type:released <file>
                                              Check compatibility. Type is one of 'api' and 'removed', which checks
-                                             either the public api or the removed api. State is one of 'current' and
-                                             'released', to check either the currently in development API or the last
-                                             publicly released API, respectively. Different compatibility checks apply
-                                             in the two scenarios. For example, to check the code base against the
-                                             current public API, use --check-compatibility:api:current.
+                                             either the public api or the removed api.
 --check-compatibility:base <file>
                                              When performing a compat check, use the provided signature file as a base
                                              api, which is treated as part of the API being checked. This allows us to
@@ -331,12 +327,6 @@
 --extract-annotations <zipfile>
                                              Extracts source annotations from the source files and writes them into the
                                              given zip file
---include-annotation-classes <dir>
-                                             Copies the given stub annotation source files into the generated stub
-                                             sources; <dir> is typically metalava/stub-annotations/src/main/java/.
---rewrite-annotations <dir/jar>
-                                             For a bytecode folder or output jar, rewrites the androidx annotations to
-                                             be package private
 --force-convert-to-warning-nullability-annotations <package1:-package2:...>
                                              On every API declared in a class referenced by the given filter, makes
                                              nullability issues appear to callers as warnings rather than errors by
@@ -361,6 +351,11 @@
 --generate-api-levels <xmlfile>
                                              Reads android.jar SDK files and generates an XML file recording the API
                                              level for each class, method and field
+--remove-missing-class-references-in-api-levels
+                                             Removes references to missing classes when generating the API levels XML
+                                             file. This can happen when generating the XML file for the non-updatable
+                                             portions of the module-lib sdk, as those non-updatable portions can
+                                             reference classes that are part of an updatable apex.
 --android-jar-pattern <pattern>
                                              Patterns to use to locate Android JAR files. The default is
                                              ${"$"}ANDROID_HOME/platforms/android-%/android.jar.
@@ -372,6 +367,27 @@
                                              Sets the code name for the current source code
 --current-jar
                                              Points to the current API jar, if any
+--sdk-extensions-root
+                                             Points to root of prebuilt extension SDK jars, if any. This directory is
+                                             expected to contain snapshots of historical extension SDK versions in the
+                                             form of stub jars. The paths should be on the format
+                                             "<int>/public/<module-name>.jar", where <int> corresponds to the extension
+                                             SDK version, and <module-name> to the name of the mainline module.
+--sdk-extensions-info
+                                             Points to map of extension SDK APIs to include, if any. The file is a plain
+                                             text file and describes, per extension SDK, what APIs from that extension
+                                             to include in the file created via --generate-api-levels. The format of
+                                             each line is one of the following: "<module-name> <pattern> <ext-name>
+                                             [<ext-name> [...]]", where <module-name> is the name of the mainline module
+                                             this line refers to, <pattern> is a common Java name prefix of the APIs
+                                             this line refers to, and <ext-name> is a list of extension SDK names in
+                                             which these SDKs first appeared, or "<ext-name> <ext-id> <type>", where
+                                             <ext-name> is the name of an SDK, <ext-id> its numerical ID and <type> is
+                                             one of "platform" (the Android platform SDK), "platform-ext" (an extension
+                                             to the Android platform SDK), "standalone" (a separate SDK). Fields are
+                                             separated by whitespace. A mainline module may be listed multiple times.
+                                             The special pattern "*" refers to all APIs in the given mainline module.
+                                             Lines beginning with # are comments.
 
 
 Sandboxing:
diff --git a/src/test/java/com/android/tools/metalava/RewriteAnnotationsTest.kt b/src/test/java/com/android/tools/metalava/RewriteAnnotationsTest.kt
index 0f5e146..ec3aad7 100644
--- a/src/test/java/com/android/tools/metalava/RewriteAnnotationsTest.kt
+++ b/src/test/java/com/android/tools/metalava/RewriteAnnotationsTest.kt
@@ -16,17 +16,12 @@
 
 package com.android.tools.metalava
 
-import com.android.tools.lint.checks.infrastructure.TestFiles.base64gzip
-import com.android.tools.lint.checks.infrastructure.TestFiles.jar
-import com.android.tools.lint.checks.infrastructure.TestFiles.xml
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertThrows
 import org.junit.Assert.assertTrue
 import org.junit.Test
 import java.io.File
-import java.lang.reflect.Modifier
-import java.net.URLClassLoader
 import kotlin.text.Charsets.UTF_8
 
 class RewriteAnnotationsTest : DriverTest() {
@@ -135,81 +130,4 @@
             )
         }
     }
-
-    @Test
-    fun `Test rewriting the bytecode for one of the public annotations`() {
-        val bytecode = base64gzip(
-            "androidx/annotation/CallSuper.class",
-            "" +
-                "H4sIAAAAAAAAAIWPsU4CQRRF70NhEQWxJMZoLCjdxs6KIMYCA2E3NlbD8kKG" +
-                "DDNkmSXwaxZ+gB9FfGMBFps4yczc5J53kve9//wC8IirCK0IlxHahEbiijzj" +
-                "F22Y0OorY5JixfnDQm0UoTMprNdLftdrPTXcs9Z55bWza8LdMDCxUXYeq0MR" +
-                "T9izDemJUN0oU4i3+w86dkZnuzDQH/aShHBTPpCqfM5euPvyfmB4KcZ0t2KB" +
-                "am+D9HX0LDZlZ7nTs+1f9rAqoX2UjaYLzjzhttR/3L9LIFTkniCcCk5/3ypq" +
-                "8l9LiqSrM87QwHmIHyDGBZo/ObYRQoUBAAA="
-        )
-
-        val compiledStubs = temporaryFolder.newFolder("compiled-stubs")
-        bytecode.createFile(compiledStubs)
-
-        runDriver(
-            ARG_NO_COLOR,
-            ARG_NO_BANNER,
-
-            ARG_REWRITE_ANNOTATIONS,
-            compiledStubs.path,
-
-            ARG_CLASS_PATH,
-            getAndroidJar().path
-        )
-
-        // Load the class to make sure it's legit
-        val url = compiledStubs.toURI().toURL()
-        val loader = URLClassLoader(arrayOf(url), null)
-        val annotationClass = loader.loadClass("androidx.annotation.CallSuper")
-        val modifiers = annotationClass.modifiers
-        assertEquals(0, modifiers and Modifier.PUBLIC)
-        assertTrue(annotationClass.isAnnotation)
-    }
-
-    @Test
-    fun `Test rewriting the bytecode for one of the public annotations in a jar file`() {
-        val bytecode = base64gzip(
-            "androidx/annotation/CallSuper.class",
-            "" +
-                "H4sIAAAAAAAAAIWPsU4CQRRF70NhEQWxJMZoLCjdxs6KIMYCA2E3NlbD8kKG" +
-                "DDNkmSXwaxZ+gB9FfGMBFps4yczc5J53kve9//wC8IirCK0IlxHahEbiijzj" +
-                "F22Y0OorY5JixfnDQm0UoTMprNdLftdrPTXcs9Z55bWza8LdMDCxUXYeq0MR" +
-                "T9izDemJUN0oU4i3+w86dkZnuzDQH/aShHBTPpCqfM5euPvyfmB4KcZ0t2KB" +
-                "am+D9HX0LDZlZ7nTs+1f9rAqoX2UjaYLzjzhttR/3L9LIFTkniCcCk5/3ypq" +
-                "8l9LiqSrM87QwHmIHyDGBZo/ObYRQoUBAAA="
-        )
-
-        val jarDesc = jar(
-            "myjar.jar",
-            bytecode,
-            xml("foo/bar/baz.xml", "<hello-world/>")
-        )
-
-        val jarFile = jarDesc.createFile(temporaryFolder.root)
-
-        runDriver(
-            ARG_NO_COLOR,
-            ARG_NO_BANNER,
-
-            ARG_REWRITE_ANNOTATIONS,
-            jarFile.path,
-
-            ARG_CLASS_PATH,
-            getAndroidJar().path
-        )
-
-        // Load the class to make sure it's legit
-        val url = jarFile.toURI().toURL()
-        val loader = URLClassLoader(arrayOf(url), null)
-        val annotationClass = loader.loadClass("androidx.annotation.CallSuper")
-        val modifiers = annotationClass.modifiers
-        assertEquals(0, modifiers and Modifier.PUBLIC)
-        assertTrue(annotationClass.isAnnotation)
-    }
 }
diff --git a/src/test/java/com/android/tools/metalava/SubtractApiTest.kt b/src/test/java/com/android/tools/metalava/SubtractApiTest.kt
index a74c598..6bbabcd 100644
--- a/src/test/java/com/android/tools/metalava/SubtractApiTest.kt
+++ b/src/test/java/com/android/tools/metalava/SubtractApiTest.kt
@@ -77,7 +77,7 @@
                     package test.pkg;
                     @SuppressWarnings({"unchecked", "deprecation", "all"})
                     public class OnlyInNew {
-                    private OnlyInNew() { throw new RuntimeException("Stub!"); }
+                    OnlyInNew() { throw new RuntimeException("Stub!"); }
                     public void method1() { throw new RuntimeException("Stub!"); }
                     public void method5() { throw new RuntimeException("Stub!"); }
                     public void method6() { throw new RuntimeException("Stub!"); }
diff --git a/src/test/java/com/android/tools/metalava/apilevels/ApiGeneratorTest.kt b/src/test/java/com/android/tools/metalava/apilevels/ApiGeneratorTest.kt
index 813a832..e4dbec3 100644
--- a/src/test/java/com/android/tools/metalava/apilevels/ApiGeneratorTest.kt
+++ b/src/test/java/com/android/tools/metalava/apilevels/ApiGeneratorTest.kt
@@ -21,12 +21,18 @@
 import com.android.tools.metalava.ARG_CURRENT_VERSION
 import com.android.tools.metalava.ARG_FIRST_VERSION
 import com.android.tools.metalava.ARG_GENERATE_API_LEVELS
+import com.android.tools.metalava.ARG_REMOVE_MISSING_CLASS_REFERENCES_IN_API_LEVELS
+import com.android.tools.metalava.ARG_SDK_INFO_FILE
+import com.android.tools.metalava.ARG_SDK_JAR_ROOT
 import com.android.tools.metalava.DriverTest
 import com.android.tools.metalava.getApiLookup
 import com.android.tools.metalava.java
+import com.google.common.truth.Truth.assertThat
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
 import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
 import org.junit.Test
 import java.io.File
 import kotlin.text.Charsets.UTF_8
@@ -119,6 +125,55 @@
             }
         }
 
+        var extensionSdkJars = File("prebuilts/sdk/extensions")
+        if (!extensionSdkJars.isDirectory) {
+            extensionSdkJars = File("../../prebuilts/sdk/extensions")
+            if (!extensionSdkJars.isDirectory) {
+                println("Ignoring ${ApiGeneratorTest::class.java}: prebuilts not found: $extensionSdkJars")
+                return
+            }
+        }
+
+        val filter = File.createTempFile("filter", "txt")
+        filter.deleteOnExit()
+        filter.writeText(
+            """
+                <sdk-extensions-info>
+                <!-- SDK definitions -->
+                <sdk shortname="R" name="R Extensions" id="30" reference="android/os/Build${'$'}VERSION_CODES${'$'}R" />
+                <sdk shortname="S" name="S Extensions" id="31" reference="android/os/Build${'$'}VERSION_CODES${'$'}S" />
+                <sdk shortname="T" name="T Extensions" id="33" reference="android/os/Build${'$'}VERSION_CODES${'$'}T" />
+
+                <!-- Rules -->
+                <symbol jar="art.module.public.api" pattern="*" sdks="R" />
+                <symbol jar="conscrypt.module.intra.core.api " pattern="" sdks="R" />
+                <symbol jar="conscrypt.module.platform.api" pattern="*" sdks="R" />
+                <symbol jar="conscrypt.module.public.api" pattern="*" sdks="R" />
+                <symbol jar="framework-mediaprovider" pattern="*" sdks="R" />
+                <symbol jar="framework-mediaprovider" pattern="android.provider.MediaStore#canManageMedia" sdks="T" />
+                <symbol jar="framework-permission-s" pattern="*" sdks="R" />
+                <symbol jar="framework-permission" pattern="*" sdks="R" />
+                <symbol jar="framework-sdkextensions" pattern="*" sdks="R" />
+                <symbol jar="framework-scheduling" pattern="*" sdks="R" />
+                <symbol jar="framework-statsd" pattern="*" sdks="R" />
+                <symbol jar="framework-tethering" pattern="*" sdks="R" />
+                <symbol jar="legacy.art.module.platform.api" pattern="*" sdks="R" />
+                <symbol jar="service-media-s" pattern="*" sdks="R" />
+                <symbol jar="service-permission" pattern="*" sdks="R" />
+
+                <!-- use framework-permissions-s to test the order of multiple SDKs is respected -->
+                <symbol jar="android.net.ipsec.ike" pattern="android.net.eap.EapAkaInfo" sdks="R,S,T" />
+                <symbol jar="android.net.ipsec.ike" pattern="android.net.eap.EapInfo" sdks="T,S,R" />
+                <symbol jar="android.net.ipsec.ike" pattern="*" sdks="R" />
+
+                <!-- framework-connectivity: only android.net.CaptivePortal should have the 'sdks' attribute -->
+                <symbol jar="framework-connectivity" pattern="android.net.CaptivePortal" sdks="R" />
+
+                <!-- framework-media explicitly omitted: nothing in this module should have the 'sdks' attribute -->
+                </sdk-extensions-info>
+            """.trimIndent()
+        )
+
         val output = File.createTempFile("api-info", "xml")
         output.deleteOnExit()
         val outputPath = output.path
@@ -129,17 +184,35 @@
                 outputPath,
                 ARG_ANDROID_JAR_PATTERN,
                 "${platformJars.path}/%/public/android.jar",
+                ARG_SDK_JAR_ROOT,
+                "$extensionSdkJars",
+                ARG_SDK_INFO_FILE,
+                filter.path,
                 ARG_FIRST_VERSION,
-                "21"
+                "21",
+                ARG_CURRENT_VERSION,
+                "33"
             )
         )
 
         assertTrue(output.isFile)
         val xml = output.readText(UTF_8)
-        assertTrue(xml.contains("<api version=\"2\" min=\"21\">"))
+        assertTrue(xml.contains("<api version=\"3\" min=\"21\">"))
+        assertTrue(xml.contains("<sdk id=\"30\" shortname=\"R\" name=\"R Extensions\" reference=\"android/os/Build\$VERSION_CODES\$R\"/>"))
+        assertTrue(xml.contains("<sdk id=\"31\" shortname=\"S\" name=\"S Extensions\" reference=\"android/os/Build\$VERSION_CODES\$S\"/>"))
+        assertTrue(xml.contains("<sdk id=\"33\" shortname=\"T\" name=\"T Extensions\" reference=\"android/os/Build\$VERSION_CODES\$T\"/>"))
         assertTrue(xml.contains("<class name=\"android/Manifest\" since=\"21\">"))
         assertTrue(xml.contains("<field name=\"showWhenLocked\" since=\"27\"/>"))
 
+        // top level class marked as since=21 and R=1, implemented in the framework-mediaprovider mainline module
+        assertTrue(xml.contains("<class name=\"android/provider/MediaStore\" module=\"framework-mediaprovider\" since=\"21\" sdks=\"30:1,0:21\">"))
+
+        // method with identical sdks attribute as containing class: sdks attribute should be omitted
+        assertTrue(xml.contains("<method name=\"getMediaScannerUri()Landroid/net/Uri;\"/>"))
+
+        // method with different sdks attribute than containing class
+        assertTrue(xml.contains("<method name=\"canManageMedia(Landroid/content/Context;)Z\" since=\"31\" sdks=\"33:1,0:31\"/>"))
+
         val apiLookup = getApiLookup(output)
         apiLookup.getClassVersion("android.v")
         // This field was added in API level 5, but when we're starting the count higher
@@ -148,6 +221,32 @@
 
         val methodVersion = apiLookup.getMethodVersion("android/icu/util/CopticCalendar", "computeTime", "()")
         assertEquals(24, methodVersion)
+
+        // The filter says 'framework-permission-s             *    R' so RoleManager should exist and should have a module/sdks attributes
+        assertTrue(apiLookup.containsClass("android/app/role/RoleManager"))
+        assertTrue(xml.contains("<method name=\"canManageMedia(Landroid/content/Context;)Z\" since=\"31\" sdks=\"33:1,0:31\"/>"))
+
+        // The filter doesn't mention framework-media, so no class in that module should have a module/sdks attributes
+        assertTrue(xml.contains("<class name=\"android/media/MediaFeature\" since=\"31\">"))
+
+        // The filter only defines a single API in framework-connectivity: verify that only that API has the module/sdks attributes
+        assertTrue(xml.contains("<class name=\"android/net/CaptivePortal\" module=\"framework-connectivity\" since=\"23\" sdks=\"30:1,0:23\">"))
+        assertTrue(xml.contains("<class name=\"android/net/ConnectivityDiagnosticsManager\" since=\"30\">"))
+
+        // The order of the SDKs should be respected
+        // android.net.eap.EapAkaInfo    R S T -> 0,30,31,33
+        assertTrue(xml.contains("<class name=\"android/net/eap/EapAkaInfo\" module=\"android.net.ipsec.ike\" since=\"33\" sdks=\"30:3,31:3,33:3,0:33\">"))
+        // android.net.eap.EapInfo       T S R -> 0,33,31,30
+        assertTrue(xml.contains("<class name=\"android/net/eap/EapInfo\" module=\"android.net.ipsec.ike\" since=\"33\" sdks=\"33:3,31:3,30:3,0:33\">"))
+
+        // Verify historical backfill
+        assertEquals(30, apiLookup.getClassVersion("android/os/ext/SdkExtensions"))
+        assertEquals(30, apiLookup.getMethodVersion("android/os/ext/SdkExtensions", "getExtensionVersion", "(I)I"))
+        assertEquals(31, apiLookup.getMethodVersion("android/os/ext/SdkExtensions", "getAllExtensionVersions", "()Ljava/util/Map;"))
+
+        // Verify there's no extension versions listed for SdkExtensions
+        val sdkExtClassLine = xml.lines().first { it.contains("<class name=\"android/os/ext/SdkExtensions\"") }
+        assertFalse(sdkExtClassLine.contains("sdks="))
     }
 
     @Test
@@ -259,4 +358,195 @@
         val apiLookup = getApiLookup(output, temporaryFolder.newFolder())
         assertEquals(90, apiLookup.getClassVersion("android.pkg.MyTest"))
     }
+
+    @Test
+    fun `Generate API for test prebuilts`() {
+        var testPrebuiltsRoot = File(System.getenv("METALAVA_TEST_PREBUILTS_SDK_ROOT"))
+        if (!testPrebuiltsRoot.isDirectory) {
+            fail("test prebuilts not found: $testPrebuiltsRoot")
+        }
+
+        val api_versions_xml = File.createTempFile("api-versions", "xml")
+        api_versions_xml.deleteOnExit()
+
+        check(
+            extraArguments = arrayOf(
+                ARG_GENERATE_API_LEVELS,
+                api_versions_xml.path,
+                ARG_ANDROID_JAR_PATTERN,
+                "${testPrebuiltsRoot.path}/%/public/android.jar",
+                ARG_SDK_JAR_ROOT,
+                "${testPrebuiltsRoot.path}/extensions",
+                ARG_SDK_INFO_FILE,
+                "${testPrebuiltsRoot.path}/sdk-extensions-info.xml",
+                ARG_FIRST_VERSION,
+                "30",
+                ARG_CURRENT_VERSION,
+                "32",
+                ARG_CURRENT_CODENAME,
+                "Foo"
+            ),
+            sourceFiles = arrayOf(
+                java(
+                    """
+                    package android.test;
+                    public class ClassAddedInApi31AndExt2 {
+                        private ClassAddedInApi31AndExt2() {}
+                        public static final int FIELD_ADDED_IN_API_31_AND_EXT_2 = 1;
+                        public static final int FIELD_ADDED_IN_EXT_3 = 2;
+                        public void methodAddedInApi31AndExt2() { throw new RuntimeException("Stub!"); }
+                        public void methodAddedInExt3() { throw new RuntimeException("Stub!"); };
+                        public void methodNotFinalized() { throw new RuntimeException("Stub!"); }
+                    }
+                    """
+                )
+            )
+        )
+
+        assertTrue(api_versions_xml.isFile)
+        val xml = api_versions_xml.readText(UTF_8)
+
+        val expected = """
+            <?xml version="1.0" encoding="utf-8"?>
+            <api version="3" min="30">
+                <sdk id="30" shortname="R-ext" name="R Extensions" reference="android/os/Build${'$'}VERSION_CODES${'$'}R"/>
+                <sdk id="31" shortname="S-ext" name="S Extensions" reference="android/os/Build${'$'}VERSION_CODES${'$'}S"/>
+                <class name="android/test/ClassAddedInApi30" since="30">
+                    <extends name="java/lang/Object"/>
+                    <method name="methodAddedInApi30()V"/>
+                    <method name="methodAddedInApi31()V" since="31"/>
+                </class>
+                <class name="android/test/ClassAddedInApi31AndExt2" module="framework-ext" since="31" sdks="30:2,31:2,0:31">
+                    <extends name="java/lang/Object"/>
+                    <method name="methodAddedInApi31AndExt2()V"/>
+                    <method name="methodAddedInExt3()V" since="33" sdks="30:3,31:3"/>
+                    <method name="methodNotFinalized()V" since="33" sdks="0:33"/>
+                    <field name="FIELD_ADDED_IN_API_31_AND_EXT_2"/>
+                    <field name="FIELD_ADDED_IN_EXT_3" since="33" sdks="30:3,31:3"/>
+                </class>
+                <class name="android/test/ClassAddedInExt1" module="framework-ext" since="31" sdks="30:1,31:1,0:31">
+                    <extends name="java/lang/Object"/>
+                    <method name="methodAddedInApi31AndExt2()V" sdks="30:2,31:2,0:31"/>
+                    <method name="methodAddedInExt1()V"/>
+                    <method name="methodAddedInExt3()V" since="33" sdks="30:3,31:3"/>
+                    <field name="FIELD_ADDED_IN_API_31_AND_EXT_2" sdks="30:2,31:2,0:31"/>
+                    <field name="FIELD_ADDED_IN_EXT_1"/>
+                    <field name="FIELD_ADDED_IN_EXT_3" since="33" sdks="30:3,31:3"/>
+                </class>
+                <class name="android/test/ClassAddedInExt3" module="framework-ext" since="33" sdks="30:3,31:3">
+                    <extends name="java/lang/Object"/>
+                    <method name="methodAddedInExt3()V"/>
+                    <field name="FIELD_ADDED_IN_EXT_3"/>
+                </class>
+                <class name="java/lang/Object" since="30">
+                    <method name="&lt;init>()V"/>
+                </class>
+            </api>
+        """
+
+        fun String.trimEachLine(): String =
+            lines().map {
+                it.trim()
+            }.filter {
+                it.isNotEmpty()
+            }.joinToString("\n")
+
+        assertEquals(expected.trimEachLine(), xml.trimEachLine())
+    }
+
+    @Test
+    fun `Generate API while removing missing class references`() {
+        val api_versions_xml = File.createTempFile("api-versions", "xml")
+        api_versions_xml.deleteOnExit()
+
+        check(
+            extraArguments = arrayOf(
+                ARG_GENERATE_API_LEVELS,
+                api_versions_xml.path,
+                ARG_REMOVE_MISSING_CLASS_REFERENCES_IN_API_LEVELS,
+                ARG_FIRST_VERSION,
+                "30",
+                ARG_CURRENT_VERSION,
+                "32",
+                ARG_CURRENT_CODENAME,
+                "Foo"
+            ),
+            sourceFiles = arrayOf(
+                java(
+                    """
+                    package android.test;
+                    public class ClassThatImplementsMethodFromApex implements ClassFromApex {
+                    }
+                    """
+                )
+            )
+        )
+
+        assertTrue(api_versions_xml.isFile)
+        val xml = api_versions_xml.readText(UTF_8)
+
+        val expected = """
+            <?xml version="1.0" encoding="utf-8"?>
+            <api version="3" min="30">
+                <class name="android/test/ClassThatImplementsMethodFromApex" since="33">
+                    <method name="&lt;init>()V"/>
+                </class>
+            </api>
+        """
+
+        fun String.trimEachLine(): String =
+            lines().map {
+                it.trim()
+            }.filter {
+                it.isNotEmpty()
+            }.joinToString("\n")
+
+        assertEquals(expected.trimEachLine(), xml.trimEachLine())
+    }
+
+    @Test
+    fun `Generate API finds missing class references`() {
+        var testPrebuiltsRoot = File(System.getenv("METALAVA_TEST_PREBUILTS_SDK_ROOT"))
+        if (!testPrebuiltsRoot.isDirectory) {
+            fail("test prebuilts not found: $testPrebuiltsRoot")
+        }
+
+        val api_versions_xml = File.createTempFile("api-versions", "xml")
+        api_versions_xml.deleteOnExit()
+
+        var exception: IllegalStateException? = null
+        try {
+            check(
+                extraArguments = arrayOf(
+                    ARG_GENERATE_API_LEVELS,
+                    api_versions_xml.path,
+                    ARG_FIRST_VERSION,
+                    "30",
+                    ARG_CURRENT_VERSION,
+                    "32",
+                    ARG_CURRENT_CODENAME,
+                    "Foo"
+                ),
+                sourceFiles = arrayOf(
+                    java(
+                        """
+                        package android.test;
+                        // Really this class should implement some interface that doesn't exist,
+                        // but that's hard to set up in the test harness, so just verify that
+                        // metalava complains about java/lang/Object not existing because we didn't
+                        // include the testdata prebuilt jars.
+                        public class ClassThatImplementsMethodFromApex {
+                        }
+                        """
+                    )
+                )
+            )
+        } catch (e: IllegalStateException) {
+            exception = e
+        }
+
+        assertNotNull(exception)
+        assertThat(exception?.message ?: "").contains("There are classes in this API that reference other classes that do not exist in this API.")
+        assertThat(exception?.message ?: "").contains("java/lang/Object referenced by:\n    android/test/ClassThatImplementsMethodFromApex")
+    }
 }
diff --git a/src/test/java/com/android/tools/metalava/apilevels/ApiToExtensionsMapTest.kt b/src/test/java/com/android/tools/metalava/apilevels/ApiToExtensionsMapTest.kt
new file mode 100644
index 0000000..258c39e
--- /dev/null
+++ b/src/test/java/com/android/tools/metalava/apilevels/ApiToExtensionsMapTest.kt
@@ -0,0 +1,378 @@
+/*
+ * Copyright (C) 2022 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 com.android.tools.metalava.apilevels
+
+import org.junit.Assert
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertTrue
+
+class ApiToExtensionsMapTest {
+    @Test
+    fun `empty input`() {
+        val xml = """
+            <?xml version="1.0" encoding="utf-8"?>
+            <!-- No rules is a valid (albeit weird). -->
+            <sdk-extensions-info>
+                <sdk shortname="R-ext" name="R Extensions" id="30" reference="android/os/Build${'$'}VERSION_CODES${'$'}R" />
+                <sdk shortname="S-ext" name="S Extensions" id="31" reference="android/os/Build${'$'}VERSION_CODES${'$'}S" />
+                <sdk shortname="T-ext" name="T Extensions" id="33" reference="android/os/Build${'$'}VERSION_CODES${'$'}T" />
+            </sdk-extensions-info>
+        """.trimIndent()
+        val map = ApiToExtensionsMap.fromXml("no-module", xml)
+
+        assertTrue(map.getExtensions("com.foo.Bar").isEmpty())
+    }
+
+    @Test
+    fun wildcard() {
+        val xml = """
+            <?xml version="1.0" encoding="utf-8"?>
+            <!-- All APIs will default to extension SDK A. -->
+            <sdk-extensions-info>
+                <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
+                <symbol jar="mod" pattern="*" sdks="A" />
+            </sdk-extensions-info>
+        """.trimIndent()
+        val map = ApiToExtensionsMap.fromXml("mod", xml)
+
+        assertEquals(map.getExtensions("com.foo.Bar"), listOf("A"))
+        assertEquals(map.getExtensions("com.foo.SomeOtherBar"), listOf("A"))
+    }
+
+    @Test
+    fun `single class`() {
+        val xml = """
+            <?xml version="1.0" encoding="utf-8"?>
+            <!-- A single class. The class, any internal classes, and any methods are allowed;
+                 everything else is denied -->
+            <sdk-extensions-info>
+                <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
+                <symbol jar="mod" pattern="com.foo.Bar" sdks="A" />
+            </sdk-extensions-info>
+        """.trimIndent()
+        val map = ApiToExtensionsMap.fromXml("mod", xml)
+
+        assertEquals(map.getExtensions("com.foo.Bar"), listOf("A"))
+        assertEquals(map.getExtensions("com.foo.Bar#FIELD"), listOf("A"))
+        assertEquals(map.getExtensions("com.foo.Bar#method"), listOf("A"))
+        assertEquals(map.getExtensions("com.foo.Bar\$Inner"), listOf("A"))
+        assertEquals(map.getExtensions("com.foo.Bar\$Inner\$InnerInner"), listOf("A"))
+
+        val clazz = ApiClass("com/foo/Bar", 1, false)
+        val method = ApiElement("method(Ljava.lang.String;I)V", 2, false)
+        assertEquals(map.getExtensions(clazz), listOf("A"))
+        assertEquals(map.getExtensions(clazz, method), listOf("A"))
+
+        assertTrue(map.getExtensions("com.foo.SomeOtherClass").isEmpty())
+    }
+
+    @Test
+    fun `multiple extensions`() {
+        val xml = """
+            <?xml version="1.0" encoding="utf-8"?>
+            <!-- Any number of white space separated extension SDKs may be listed. -->
+            <sdk-extensions-info>
+                <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
+                <sdk shortname="B" name="B Extensions" id="2" reference="android/os/Build${'$'}VERSION_CODES${'$'}B" />
+                <sdk shortname="FOO" name="FOO Extensions" id="10" reference="android/os/Build${'$'}VERSION_CODES${'$'}FOO" />
+                <sdk shortname="BAR" name="BAR Extensions" id="11" reference="android/os/Build${'$'}VERSION_CODES${'$'}BAR" />
+                <symbol jar="mod" pattern="*" sdks="A,B,FOO,BAR" />
+            </sdk-extensions-info>
+        """.trimIndent()
+        val map = ApiToExtensionsMap.fromXml("mod", xml)
+
+        assertEquals(listOf("A", "B", "FOO", "BAR"), map.getExtensions("com.foo.Bar"))
+    }
+
+    @Test
+    fun precedence() {
+        val xml = """
+            <?xml version="1.0" encoding="utf-8"?>
+            <!-- Multiple classes, and multiple rules with different precedence. -->
+            <sdk-extensions-info>
+                <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
+                <sdk shortname="B" name="B Extensions" id="2" reference="android/os/Build${'$'}VERSION_CODES${'$'}B" />
+                <sdk shortname="C" name="C Extensions" id="3" reference="android/os/Build${'$'}VERSION_CODES${'$'}C" />
+                <sdk shortname="D" name="D Extensions" id="4" reference="android/os/Build${'$'}VERSION_CODES${'$'}D" />
+                <symbol jar="mod" pattern="*" sdks="A" />
+                <symbol jar="mod" pattern="com.foo.Bar" sdks="B" />
+                <symbol jar="mod" pattern="com.foo.Bar${'$'}Inner#method" sdks="C" />
+                <symbol jar="mod" pattern="com.bar.Foo" sdks="D" />
+            </sdk-extensions-info>
+        """.trimIndent()
+        val map = ApiToExtensionsMap.fromXml("mod", xml)
+
+        assertEquals(map.getExtensions("anything"), listOf("A"))
+
+        assertEquals(map.getExtensions("com.foo.Bar"), listOf("B"))
+        assertEquals(map.getExtensions("com.foo.Bar#FIELD"), listOf("B"))
+        assertEquals(map.getExtensions("com.foo.Bar\$Inner"), listOf("B"))
+
+        assertEquals(map.getExtensions("com.foo.Bar\$Inner#method"), listOf("C"))
+
+        assertEquals(map.getExtensions("com.bar.Foo"), listOf("D"))
+        assertEquals(map.getExtensions("com.bar.Foo#FIELD"), listOf("D"))
+    }
+
+    @Test
+    fun `multiple mainline modules`() {
+        val xml = """
+            <?xml version="1.0" encoding="utf-8"?>
+            <!-- The allow list will only consider patterns that are marked with the given mainline module -->
+            <sdk-extensions-info>
+                <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
+                <sdk shortname="B" name="B Extensions" id="2" reference="android/os/Build${'$'}VERSION_CODES${'$'}B" />
+                <symbol jar="foo" pattern="*" sdks="A" />
+                <symbol jar="bar" pattern="*" sdks="B" />
+            </sdk-extensions-info>
+        """.trimIndent()
+        val allowListA = ApiToExtensionsMap.fromXml("foo", xml)
+        val allowListB = ApiToExtensionsMap.fromXml("bar", xml)
+        val allowListC = ApiToExtensionsMap.fromXml("baz", xml)
+
+        assertEquals(allowListA.getExtensions("anything"), listOf("A"))
+        assertEquals(allowListB.getExtensions("anything"), listOf("B"))
+        assertTrue(allowListC.getExtensions("anything").isEmpty())
+    }
+
+    @Test
+    fun `declarations and rules can be mixed`() {
+        val xml = """
+            <?xml version="1.0" encoding="utf-8"?>
+            <!-- SDK declarations and rule lines can be mixed in any order -->
+            <sdk-extensions-info>
+                <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
+                <symbol jar="foo" pattern="*" sdks="A,B" />
+                <sdk shortname="B" name="B Extensions" id="2" reference="android/os/Build${'$'}VERSION_CODES${'$'}B" />
+            </sdk-extensions-info>
+        """.trimIndent()
+        val map = ApiToExtensionsMap.fromXml("foo", xml)
+
+        assertEquals(map.getExtensions("com.foo.Bar"), listOf("A", "B"))
+    }
+
+    @Test
+    fun `bad input`() {
+        assertFailsWith<IllegalArgumentException> {
+            ApiToExtensionsMap.fromXml(
+                "mod",
+                """
+                    <?xml version="1.0" encoding="utf-8"?>
+                    <!-- Missing root element -->
+                    <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
+                    <symbol jar="mod" pattern="com.foo.Bar" sdks="A" />
+                """.trimIndent()
+            )
+        }
+
+        assertFailsWith<IllegalArgumentException> {
+            ApiToExtensionsMap.fromXml(
+                "mod",
+                """
+                    <?xml version="1.0" encoding="utf-8"?>
+                    <!-- <sdk> tag at unexpected depth  -->
+                    <sdk-extensions-info version="2">
+                        <foo>
+                            <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" >
+                        </foo>
+                        <symbol jar="mod" pattern="com.foo.Bar" sdks="A" />
+                    </sdk-extensions-info>
+                """.trimIndent()
+            )
+        }
+
+        assertFailsWith<IllegalArgumentException> {
+            ApiToExtensionsMap.fromXml(
+                "mod",
+                """
+                    <?xml version="1.0" encoding="utf-8"?>
+                    <!-- using 0 (reserved for the Android platform SDK) as ID -->
+                    <sdk-extensions-info>
+                        <sdk shortname="A" name="A Extensions" id="0" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
+                        <symbol jar="mod" pattern="com.foo.Bar" sdks="A" />
+                    </sdk-extensions-info>
+                """.trimIndent()
+            )
+        }
+
+        assertFailsWith<IllegalArgumentException> {
+            ApiToExtensionsMap.fromXml(
+                "mod",
+                """
+                    <?xml version="1.0" encoding="utf-8"?>
+                    <!-- missing module attribute -->
+                    <sdk-extensions-info>
+                        <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
+                        <symbol pattern="com.foo.Bar" sdks="A" />
+                    </sdk-extensions-info>
+                """.trimIndent()
+            )
+        }
+
+        assertFailsWith<IllegalArgumentException> {
+            ApiToExtensionsMap.fromXml(
+                "mod",
+                """
+                    <?xml version="1.0" encoding="utf-8"?>
+                    <!-- duplicate module+pattern pairs -->
+                    <sdk-extensions-info>
+                        <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
+                        <symbol jar="mod" pattern="com.foo.Bar" sdks="A" />
+                        <symbol jar="mod" pattern="com.foo.Bar" sdks="B" />
+                    </sdk-extensions-info>
+                """.trimIndent()
+            )
+        }
+
+        assertFailsWith<IllegalArgumentException> {
+            ApiToExtensionsMap.fromXml(
+                "mod",
+                """
+                    <?xml version="1.0" encoding="utf-8"?>
+                    <!-- sdks attribute refer to non-declared SDK -->
+                    <sdk-extensions-info>
+                        <sdk shortname="B" name="A Extensions" id="2" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
+                        <symbol jar="mod" pattern="com.foo.Bar" sdks="A" />
+                    </sdk-extensions-info>
+                """.trimIndent()
+            )
+        }
+
+        assertFailsWith<IllegalArgumentException> {
+            ApiToExtensionsMap.fromXml(
+                "mod",
+                """
+                    <?xml version="1.0" encoding="utf-8"?>
+                    <!-- duplicate numerical ID -->
+                    <sdk-extensions-info>
+                        <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
+                        <sdk shortname="B" name="B Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}B" />
+                        <symbol jar="mod" pattern="com.foo.Bar" sdks="A" />
+                    </sdk-extensions-info>
+                """.trimIndent()
+            )
+        }
+
+        assertFailsWith<IllegalArgumentException> {
+            ApiToExtensionsMap.fromXml(
+                "mod",
+                """
+                    <?xml version="1.0" encoding="utf-8"?>
+                    <!-- duplicate short SDK name -->
+                    <sdk-extensions-info>
+                        <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
+                        <sdk shortname="A" name="B Extensions" id="2" reference="android/os/Build${'$'}VERSION_CODES${'$'}B" />
+                        <symbol jar="mod" pattern="com.foo.Bar" sdks="A" />
+                    </sdk-extensions-info>
+                """.trimIndent()
+            )
+        }
+
+        assertFailsWith<IllegalArgumentException> {
+            ApiToExtensionsMap.fromXml(
+                "mod",
+                """
+                    <?xml version="1.0" encoding="utf-8"?>
+                    <!-- duplicate long SDK name -->
+                    <sdk-extensions-info>
+                        <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
+                        <sdk shortname="B" name="A Extensions" id="2" reference="android/os/Build${'$'}VERSION_CODES${'$'}B" />
+                        <symbol jar="mod" pattern="com.foo.Bar" sdks="A" />
+                    </sdk-extensions-info>
+                """.trimIndent()
+            )
+        }
+
+        assertFailsWith<IllegalArgumentException> {
+            ApiToExtensionsMap.fromXml(
+                "mod",
+                """
+                    <?xml version="1.0" encoding="utf-8"?>
+                    <!-- duplicate SDK reference -->
+                    <sdk-extensions-info version="1">
+                        <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
+                        <sdk shortname="B" name="B Extensions" id="2" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
+                        <symbol jar="mod" pattern="com.foo.Bar" sdks="A" />
+                    </sdk-extensions-info>
+                """.trimIndent()
+            )
+        }
+
+        assertFailsWith<IllegalArgumentException> {
+            ApiToExtensionsMap.fromXml(
+                "mod",
+                """
+                    <?xml version="1.0" encoding="utf-8"?>
+                    <!-- duplicate SDK for same symbol -->
+                    <sdk-extensions-info>
+                        <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
+                        <sdk shortname="B" name="B Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}B" />
+                        <symbol jar="mod" pattern="com.foo.Bar" sdks="A,B,A" />
+                    </sdk-extensions-info>
+                """.trimIndent()
+            )
+        }
+    }
+
+    @Test
+    fun `calculate sdks xml attribute`() {
+        val xml = """
+            <?xml version="1.0" encoding="utf-8"?>
+            <!-- Verify the calculateSdksAttr method -->
+            <sdk-extensions-info>
+                <sdk shortname="R" name="R Extensions" id="30" reference="android/os/Build${'$'}VERSION_CODES${'$'}R" />
+                <sdk shortname="S" name="S Extensions" id="31" reference="android/os/Build${'$'}VERSION_CODES${'$'}S" />
+                <sdk shortname="T" name="T Extensions" id="33" reference="android/os/Build${'$'}VERSION_CODES${'$'}T" />
+                <sdk shortname="FOO" name="FOO Extensions" id="1000" reference="android/os/Build${'$'}VERSION_CODES${'$'}FOO" />
+                <sdk shortname="BAR" name="BAR Extensions" id="1001" reference="android/os/Build${'$'}VERSION_CODES${'$'}BAR" />
+            </sdk-extensions-info>
+        """.trimIndent()
+        val filter = ApiToExtensionsMap.fromXml("mod", xml)
+
+        Assert.assertEquals(
+            "0:34",
+            filter.calculateSdksAttr(34, 34, listOf(), ApiElement.NEVER)
+        )
+
+        Assert.assertEquals(
+            "30:4",
+            filter.calculateSdksAttr(34, 34, listOf("R"), 4)
+        )
+
+        Assert.assertEquals(
+            "30:4,31:4",
+            filter.calculateSdksAttr(34, 34, listOf("R", "S"), 4)
+        )
+
+        Assert.assertEquals(
+            "30:4,31:4,0:33",
+            filter.calculateSdksAttr(33, 34, listOf("R", "S"), 4)
+        )
+
+        Assert.assertEquals(
+            "30:4,31:4,1000:4,0:33",
+            filter.calculateSdksAttr(33, 34, listOf("R", "S", "FOO"), 4)
+        )
+
+        Assert.assertEquals(
+            "30:4,31:4,1000:4,1001:4,0:33",
+            filter.calculateSdksAttr(33, 34, listOf("R", "S", "FOO", "BAR"), 4)
+        )
+    }
+}
diff --git a/src/test/java/com/android/tools/metalava/apilevels/ExtensionSdkJarReaderTest.kt b/src/test/java/com/android/tools/metalava/apilevels/ExtensionSdkJarReaderTest.kt
new file mode 100644
index 0000000..b6e6b6a
--- /dev/null
+++ b/src/test/java/com/android/tools/metalava/apilevels/ExtensionSdkJarReaderTest.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 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 com.android.tools.metalava.apilevels
+
+import org.junit.Assert.assertEquals
+import java.io.File
+import java.io.FileNotFoundException
+import java.nio.file.Files
+import kotlin.test.Test
+
+class ExtensionSdkJarReaderTest {
+    @Test
+    fun `Verify findExtensionSdkJarFiles`() {
+        TemporaryDirectoryHierarchy(
+            listOf(
+                "1/public/foo.jar",
+                "1/public/bar.jar",
+                "2/public/foo.jar",
+                "2/public/bar.jar",
+                "2/public/baz.jar",
+            )
+        ).use {
+            val root = it.root
+            val expected = mapOf(
+                "foo" to listOf(
+                    VersionAndPath(1, File(root, "1/public/foo.jar")),
+                    VersionAndPath(2, File(root, "2/public/foo.jar"))
+                ),
+                "bar" to listOf(
+                    VersionAndPath(1, File(root, "1/public/bar.jar")),
+                    VersionAndPath(2, File(root, "2/public/bar.jar"))
+                ),
+                "baz" to listOf(
+                    VersionAndPath(2, File(root, "2/public/baz.jar"))
+                ),
+            )
+            val actual = ExtensionSdkJarReader.findExtensionSdkJarFiles(root)
+            assertEquals(expected, actual)
+        }
+    }
+}
+
+private class TemporaryDirectoryHierarchy(filenames: List<String>) : AutoCloseable {
+    val root: File
+
+    init {
+        root = Files.createTempDirectory("metalava").toFile()
+        for (file in filenames.map { File(root, it) }) {
+            createDirectoryRecursively(file.parentFile)
+            file.createNewFile()
+        }
+    }
+
+    override fun close() {
+        deleteDirectoryRecursively(root)
+    }
+
+    companion object {
+        private fun createDirectoryRecursively(file: File) {
+            val parent = file.parentFile ?: throw FileNotFoundException("$file has no parent")
+            if (!parent.exists()) {
+                createDirectoryRecursively(parent)
+            }
+            file.mkdir()
+        }
+
+        private fun deleteDirectoryRecursively(root: File) {
+            for (file in root.listFiles()) {
+                if (file.isDirectory()) {
+                    deleteDirectoryRecursively(file)
+                } else {
+                    file.delete()
+                }
+            }
+            root.delete()
+        }
+    }
+}
diff --git a/src/test/java/com/android/tools/metalava/binarycompatibility/BinaryCompatibilityClassMethodsAndConstructors.kt b/src/test/java/com/android/tools/metalava/binarycompatibility/BinaryCompatibilityClassMethodsAndConstructors.kt
index 758d5f0..0246db0 100644
--- a/src/test/java/com/android/tools/metalava/binarycompatibility/BinaryCompatibilityClassMethodsAndConstructors.kt
+++ b/src/test/java/com/android/tools/metalava/binarycompatibility/BinaryCompatibilityClassMethodsAndConstructors.kt
@@ -281,8 +281,11 @@
         )
     }
     @Test
-    fun `Change final to non-final (Compatible)`() {
+    fun `Change final to non-final (Compatible but Disallowed)`() {
         check(
+            expectedIssues = """
+               TESTROOT/load-api.txt:3: error: Method test.pkg.Foo.bar has removed 'final' qualifier [RemovedFinal]
+            """,
             signatureSource = """
                 package test.pkg {
                   class Foo {
diff --git a/src/test/java/com/android/tools/metalava/binarycompatibility/BinaryCompatibilityClassesTest.kt b/src/test/java/com/android/tools/metalava/binarycompatibility/BinaryCompatibilityClassesTest.kt
index 1765f17..afb4e0f 100644
--- a/src/test/java/com/android/tools/metalava/binarycompatibility/BinaryCompatibilityClassesTest.kt
+++ b/src/test/java/com/android/tools/metalava/binarycompatibility/BinaryCompatibilityClassesTest.kt
@@ -470,8 +470,11 @@
     }
 
     @Test
-    fun `Change final to non-final (Compatible)`() {
+    fun `Change final to non-final (Compatible but Disallowed)`() {
         check(
+            expectedIssues = """
+                TESTROOT/load-api.txt:3: error: Constructor test.pkg.Foo has removed 'final' qualifier [RemovedFinal]
+            """,
             signatureSource = """
                 package test.pkg {
                     public class Foo {
diff --git a/src/test/java/com/android/tools/metalava/model/AnnotationItemTest.kt b/src/test/java/com/android/tools/metalava/model/AnnotationItemTest.kt
index 3231fbc..7cf4aca 100644
--- a/src/test/java/com/android/tools/metalava/model/AnnotationItemTest.kt
+++ b/src/test/java/com/android/tools/metalava/model/AnnotationItemTest.kt
@@ -28,7 +28,7 @@
 
     @Test
     fun `Test shortenAnnotation and unshortenAnnotation`() {
-        checkShortenAnnotation("@Nullable", "@androidx.annotation.Nullable")
+        checkShortenAnnotation("@Nullable", "@android.annotation.Nullable")
         checkShortenAnnotation("@Deprecated", "@java.lang.Deprecated")
         checkShortenAnnotation("@SystemService", "@android.annotation.SystemService")
         checkShortenAnnotation("@TargetApi", "@android.annotation.TargetApi")
diff --git a/src/test/java/com/android/tools/metalava/model/psi/JavadocTest.kt b/src/test/java/com/android/tools/metalava/model/psi/JavadocTest.kt
index 342b8ab..f3894fd 100644
--- a/src/test/java/com/android/tools/metalava/model/psi/JavadocTest.kt
+++ b/src/test/java/com/android/tools/metalava/model/psi/JavadocTest.kt
@@ -31,7 +31,6 @@
         extraArguments: Array<String> = emptyArray(),
         docStubs: Boolean = false,
         showAnnotations: Array<String> = emptyArray(),
-        includeSourceRetentionAnnotations: Boolean = true,
         skipEmitPackages: List<String> = listOf("java.lang", "java.util", "java.io"),
         sourceFiles: Array<TestFile>
     ) {
@@ -44,7 +43,6 @@
             api = api,
             extraArguments = extraArguments,
             docStubs = docStubs,
-            includeSourceRetentionAnnotations = includeSourceRetentionAnnotations,
             skipEmitPackages = skipEmitPackages
         )
     }
diff --git a/src/test/java/com/android/tools/metalava/model/psi/PsiMethodItemTest.kt b/src/test/java/com/android/tools/metalava/model/psi/PsiMethodItemTest.kt
index 390643f..6a99f85 100644
--- a/src/test/java/com/android/tools/metalava/model/psi/PsiMethodItemTest.kt
+++ b/src/test/java/com/android/tools/metalava/model/psi/PsiMethodItemTest.kt
@@ -16,8 +16,10 @@
 
 package com.android.tools.metalava.model.psi
 
+import com.android.tools.metalava.java
 import com.android.tools.metalava.kotlin
 import org.junit.Test
+import kotlin.test.assertEquals
 import kotlin.test.assertNotNull
 import kotlin.test.assertNull
 import kotlin.test.assertSame
@@ -48,4 +50,29 @@
             assertNull(component1.property)
         }
     }
+
+    @Test
+    fun `method return type is non-null`() {
+        val codebase = java(
+            """
+            public class Foo {
+                public Foo() {}
+                public void bar() {}
+            }
+            """
+        )
+        testCodebase(codebase) { c ->
+            val ctorItem = c.assertClass("Foo").findMethod("Foo", "")
+            val ctorReturnType = ctorItem!!.returnType()
+
+            val methodItem = c.assertClass("Foo").findMethod("bar", "")
+            val methodReturnType = methodItem!!.returnType()
+
+            assertNotNull(ctorReturnType)
+            assertEquals("Foo", ctorReturnType.toString(), "Return type of the constructor item must be the containing class.")
+
+            assertNotNull(methodReturnType)
+            assertEquals("void", methodReturnType.toString(), "Return type of an method item should match the expected value.")
+        }
+    }
 }
diff --git a/src/test/java/com/android/tools/metalava/model/text/TextClassItemTest.kt b/src/test/java/com/android/tools/metalava/model/text/TextClassItemTest.kt
new file mode 100644
index 0000000..1689ac4
--- /dev/null
+++ b/src/test/java/com/android/tools/metalava/model/text/TextClassItemTest.kt
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 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 com.android.tools.metalava.model.text
+
+import org.junit.Test
+import kotlin.test.assertTrue
+
+class TextClassItemTest {
+    @Test
+    fun `test hasEqualReturnType() when return types are derived from interface type variables`() {
+        val codebase = ApiFile.parseApi(
+            "test",
+            """
+            package java.lang {
+              public final class Float extends java.lang.Number implements java.lang.Comparable<java.lang.Float> {
+              }
+            }
+            package java.time {
+              public final class LocalDateTime implements java.time.chrono.ChronoLocalDateTime<java.time.LocalDate> java.io.Serializable java.time.temporal.Temporal java.time.temporal.TemporalAdjuster {
+                method public java.time.LocalDate toLocalDate();
+              }
+            }
+            package java.time.chrono {
+              public interface ChronoLocalDateTime<D extends java.time.chrono.ChronoLocalDate> extends java.time.temporal.Temporal java.lang.Comparable<java.time.chrono.ChronoLocalDateTime<?>> java.time.temporal.TemporalAdjuster {
+                method public D toLocalDate();
+              }
+            }
+            package android.animation {
+              public interface TypeEvaluator<T> {
+                method public T evaluate(float, T, T);
+              }
+              public class FloatEvaluator implements android.animation.TypeEvaluator<java.lang.Number> {
+                method public Float evaluate(float, Number, Number);
+              }
+            }
+            package android.widget {
+              public abstract class AdapterView<T extends android.widget.Adapter> extends android.view.ViewGroup {
+                method public abstract T getAdapter();
+              }
+              public abstract class AbsListView extends android.widget.AdapterView<android.widget.ListAdapter> implements android.widget.Filter.FilterListener android.text.TextWatcher android.view.ViewTreeObserver.OnGlobalLayoutListener android.view.ViewTreeObserver.OnTouchModeChangeListener {
+              }
+              public interface ListAdapter extends android.widget.Adapter {
+              }
+              @android.widget.RemoteViews.RemoteView public class ListView extends android.widget.AbsListView {
+                method public android.widget.ListAdapter getAdapter();
+              }
+            }
+            package android.content {
+              public abstract class AsyncTaskLoader<D> extends android.content.Loader<D> {
+                method public abstract D loadInBackground();
+              }
+              public class CursorLoader extends android.content.AsyncTaskLoader<android.database.Cursor> {
+                method public android.database.Cursor loadInBackground();
+              }
+            }
+            package android.database {
+              public final class CursorJoiner implements java.lang.Iterable<android.database.CursorJoiner.Result> java.util.Iterator<android.database.CursorJoiner.Result> {
+                method public android.database.CursorJoiner.Result next();
+              }
+            }
+            package java.util {
+              public interface Iterator<E> {
+                method public E next();
+              }
+            }
+            package java.lang.invoke {
+              public final class MethodType implements java.io.Serializable java.lang.invoke.TypeDescriptor.OfMethod<java.lang.Class<?>,java.lang.invoke.MethodType> {
+                method public java.lang.invoke.MethodType changeParameterType(int, Class<?>);
+                method public Class<?>[] parameterArray();
+              }
+              public static interface TypeDescriptor.OfMethod<F extends java.lang.invoke.TypeDescriptor.OfField<F>, M extends java.lang.invoke.TypeDescriptor.OfMethod<F, M>> extends java.lang.invoke.TypeDescriptor {
+                method public M changeParameterType(int, F);
+                method public F[] parameterArray();
+              }
+            }
+            """.trimIndent(),
+            false
+        )
+
+        val toLocalDate1 = codebase.getOrCreateClass("java.time.LocalDateTime").findMethod("toLocalDate", "")!!
+        val toLocalDate2 = codebase.getOrCreateClass("java.time.chrono.ChronoLocalDateTime").findMethod("toLocalDate", "")!!
+        val evaluate1 = codebase.getOrCreateClass("android.animation.TypeEvaluator<T>").findMethod("evaluate", "float, T, T")!!
+        val evaluate2 = codebase.getOrCreateClass("android.animation.FloatEvaluator").findMethod("evaluate", "float, java.lang.Number, java.lang.Number")!!
+        val loadInBackground1 = codebase.getOrCreateClass("android.content.AsyncTaskLoader<D>").findMethod("loadInBackground", "")!!
+        val loadInBackground2 = codebase.getOrCreateClass("android.content.CursorLoader").findMethod("loadInBackground", "")!!
+        val next1 = codebase.getOrCreateClass("android.database.CursorJoiner").findMethod("next", "")!!
+        val next2 = codebase.getOrCreateClass("java.util.Iterator<E>").findMethod("next", "")!!
+        val changeParameterType1 = codebase.getOrCreateClass("java.lang.invoke.MethodType").findMethod("changeParameterType", "int, java.lang.Class")!!
+        val changeParameterType2 = codebase.getOrCreateClass("java.lang.invoke.TypeDescriptor.OfMethod").findMethod("changeParameterType", "int, java.lang.invoke.TypeDescriptor.OfField")!!
+        val parameterArray1 = codebase.getOrCreateClass("java.lang.invoke.MethodType").findMethod("parameterArray", "")!!
+        val parameterArray2 = codebase.getOrCreateClass("java.lang.invoke.TypeDescriptor.OfMethod").findMethod("parameterArray", "")!!
+
+        assertTrue(TextClassItem.hasEqualReturnType(toLocalDate1, toLocalDate2))
+        assertTrue(TextClassItem.hasEqualReturnType(evaluate1, evaluate2))
+        assertTrue(TextClassItem.hasEqualReturnType(loadInBackground1, loadInBackground2))
+        assertTrue(TextClassItem.hasEqualReturnType(next1, next2))
+        assertTrue(TextClassItem.hasEqualReturnType(changeParameterType1, changeParameterType2))
+        assertTrue(TextClassItem.hasEqualReturnType(parameterArray1, parameterArray2))
+    }
+
+    @Test
+    fun `test hasEqualReturnType() with equal bounds return types`() {
+        val codebase = ApiFile.parseApi(
+            "test",
+            """
+            package java.lang {
+              public final class Class<T> implements java.lang.reflect.AnnotatedElement {
+                method @Nullable public <A extends java.lang.annotation.Annotation> A getAnnotation(@NonNull Class<A>);
+              }
+              public interface AnnotatedElement {
+                method @Nullable public <T extends java.lang.annotation.Annotation> T getAnnotation(@NonNull Class<T>);
+              }
+            }
+            """.trimIndent(),
+            false
+        )
+
+        val getAnnotation1 = codebase.getOrCreateClass("java.lang.Class<T>").findMethod("getAnnotation", "java.lang.Class")!!
+        val getAnnotation2 = codebase.getOrCreateClass("java.lang.AnnotatedElement").findMethod("getAnnotation", "java.lang.Class")!!
+
+        assertTrue(TextClassItem.hasEqualReturnType(getAnnotation1, getAnnotation2))
+    }
+
+    @Test
+    fun `test hasEqualReturnType() with covariant return types`() {
+        val codebase = ApiFile.parseApi(
+            "test",
+            """
+            package android.widget {
+              public abstract class AdapterView<T extends android.widget.Adapter> extends android.view.ViewGroup {
+                method public abstract T getAdapter();
+              }
+              public abstract class AbsListView extends android.widget.AdapterView<android.widget.ListAdapter> implements android.widget.Filter.FilterListener android.text.TextWatcher android.view.ViewTreeObserver.OnGlobalLayoutListener android.view.ViewTreeObserver.OnTouchModeChangeListener {
+              }
+              public interface ListAdapter extends android.widget.Adapter {
+              }
+              @android.widget.RemoteViews.RemoteView public class ListView extends android.widget.AbsListView {
+                method public android.widget.ListAdapter getAdapter();
+              }
+            }
+            """.trimIndent(),
+            false
+        )
+
+        val getAdapter1 = codebase.getOrCreateClass("android.widget.AdapterView<T extends android.widget.Adapter>").findMethod("getAdapter", "")!!
+        val getAdapter2 = codebase.getOrCreateClass("android.widget.ListView").findMethod("getAdapter", "")!!
+
+        assertTrue(TextClassItem.hasEqualReturnType(getAdapter1, getAdapter2))
+    }
+
+    @Test
+    fun `test equalMethodInClassContext()`() {
+        val codebase = ApiFile.parseApi(
+            "test",
+            """
+            package java.lang {
+              public interface Comparable<T> {
+                method public int compareTo(T);
+              }
+              public final class String implements java.lang.CharSequence java.lang.Comparable<java.lang.String> java.io.Serializable {
+                method public int compareTo(@NonNull String);
+              }
+            }
+            package java.lang.invoke {
+              public final class MethodType implements java.io.Serializable java.lang.invoke.TypeDescriptor.OfMethod<java.lang.Class<?>,java.lang.invoke.MethodType> {
+                method public java.lang.invoke.MethodType insertParameterTypes(int, Class<?>...);
+              }
+              public static interface TypeDescriptor.OfMethod<F extends java.lang.invoke.TypeDescriptor.OfField<F>, M extends java.lang.invoke.TypeDescriptor.OfMethod<F, M>> extends java.lang.invoke.TypeDescriptor {
+                method public M insertParameterTypes(int, F...);
+              }
+            }
+            package android.animation {
+              public interface TypeEvaluator<T> {
+                method public T evaluate(float, T, T);
+              }
+              public class ArgbEvaluator implements android.animation.TypeEvaluator {
+                method public Object evaluate(float, Object, Object);
+              }
+              public class FloatArrayEvaluator implements android.animation.TypeEvaluator<float[]> {
+                method public float[] evaluate(float, float[], float[]);
+              }
+            }
+            """.trimIndent(),
+            false
+        )
+
+        val compareTo1 = codebase.getOrCreateClass("java.lang.Comparable").findMethod("compareTo", "T")!!
+        val compareTo2 = codebase.getOrCreateClass("java.lang.String").findMethod("compareTo", "java.lang.String")!!
+        val insertParameterTypes1 = codebase.getOrCreateClass("java.lang.invoke.MethodType").findMethod("insertParameterTypes", "int, java.lang.Class...")!!
+        val insertParameterTypes2 = codebase.getOrCreateClass("java.lang.invoke.TypeDescriptor.OfMethod").findMethod("insertParameterTypes", "int, F...")!!
+        val evaluate1 = codebase.getOrCreateClass("android.animation.TypeEvaluator<T>").findMethod("evaluate", "float, T, T")!!
+        val evaluate2 = codebase.getOrCreateClass("android.animation.ArgbEvaluator").findMethod("evaluate", "float, java.lang.Object, java.lang.Object")!!
+        val evaluate3 = codebase.getOrCreateClass("android.animation.FloatArrayEvaluator").findMethod("evaluate", "float, float[], float[]")!!
+
+        assertTrue(TextClassItem.equalMethodInClassContext(compareTo1, compareTo2))
+        assertTrue(TextClassItem.equalMethodInClassContext(insertParameterTypes1, insertParameterTypes2))
+        assertTrue(TextClassItem.equalMethodInClassContext(evaluate1, evaluate2))
+        assertTrue(TextClassItem.equalMethodInClassContext(evaluate1, evaluate3))
+    }
+}
diff --git a/src/test/java/com/android/tools/metalava/model/text/TextMethodItemTest.kt b/src/test/java/com/android/tools/metalava/model/text/TextMethodItemTest.kt
new file mode 100644
index 0000000..ab9db49
--- /dev/null
+++ b/src/test/java/com/android/tools/metalava/model/text/TextMethodItemTest.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 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 com.android.tools.metalava.model.text
+
+import org.junit.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+
+class TextMethodItemTest {
+    @Test
+    fun `text method item return type is non-null`() {
+        val codebase = ApiFile.parseApi(
+            "test",
+            """
+            package test.pkg {
+              public class Foo {
+                ctor public Foo();
+                method public void bar();
+              }
+            }
+            """.trimIndent(),
+            false
+        )
+
+        val cls = codebase.findClass("test.pkg.Foo")!!
+        val ctorItem = cls.findMethod("Foo", "")!!
+        val methodItem = cls.findMethod("bar", "")!!
+
+        assertNotNull(ctorItem.returnType())
+        assertEquals("test.pkg.Foo", ctorItem.returnType().toString(), "Return type of the constructor item must be the containing class.")
+        assertNotNull(methodItem.returnType())
+        assertEquals("void", methodItem.returnType().toString(), "Return type of an method item should match the expected value.")
+    }
+}
diff --git a/src/test/java/com/android/tools/metalava/stub/StubsTest.kt b/src/test/java/com/android/tools/metalava/stub/StubsTest.kt
index 59a2881..5fddbfc 100644
--- a/src/test/java/com/android/tools/metalava/stub/StubsTest.kt
+++ b/src/test/java/com/android/tools/metalava/stub/StubsTest.kt
@@ -20,6 +20,7 @@
 
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest.source
 import com.android.tools.lint.checks.infrastructure.TestFile
+import com.android.tools.metalava.ANDROIDX_NONNULL
 import com.android.tools.metalava.ARG_CHECK_API
 import com.android.tools.metalava.ARG_EXCLUDE_ALL_ANNOTATIONS
 import com.android.tools.metalava.ARG_EXCLUDE_ANNOTATION
@@ -34,14 +35,11 @@
 import com.android.tools.metalava.deprecatedForSdkSource
 import com.android.tools.metalava.extractRoots
 import com.android.tools.metalava.gatherSources
-import com.android.tools.metalava.intDefAnnotationSource
-import com.android.tools.metalava.intRangeAnnotationSource
 import com.android.tools.metalava.java
 import com.android.tools.metalava.kotlin
 import com.android.tools.metalava.libcoreNonNullSource
 import com.android.tools.metalava.model.SUPPORT_TYPE_USE_ANNOTATIONS
 import com.android.tools.metalava.requiresApiSource
-import com.android.tools.metalava.requiresPermissionSource
 import com.android.tools.metalava.restrictToSource
 import com.android.tools.metalava.supportParameterName
 import com.android.tools.metalava.systemApiSource
@@ -58,30 +56,59 @@
     // TODO: test @DocOnly handling
 
     private fun checkStubs(
-        @Language("JAVA") source: String,
+        // source is a wrapper for stubFiles. When passing multiple stub Java files to test,
+        // use stubFiles.
+        @Language("JAVA") source: String = "",
+        stubFiles: Array<TestFile> = emptyArray(),
         warnings: String? = "",
         api: String? = null,
         extraArguments: Array<String> = emptyArray(),
         docStubs: Boolean = false,
         showAnnotations: Array<String> = emptyArray(),
-        includeSourceRetentionAnnotations: Boolean = true,
         skipEmitPackages: List<String> = listOf("java.lang", "java.util", "java.io"),
         format: FileFormat = FileFormat.latest,
-        sourceFiles: Array<TestFile>
+        sourceFiles: Array<TestFile> = emptyArray(),
+        signatureSources: Array<String> = emptyArray(),
+        checkTextStubEquivalence: Boolean = false
     ) {
+        val stubFilesArr = if (source.isNotEmpty()) arrayOf(java(source)) else stubFiles
         check(
             sourceFiles = sourceFiles,
+            signatureSources = signatureSources,
             showAnnotations = showAnnotations,
-            stubFiles = arrayOf(java(source)),
+            stubFiles = stubFilesArr,
             expectedIssues = warnings,
             checkCompilation = true,
             api = api,
             extraArguments = extraArguments,
             docStubs = docStubs,
-            includeSourceRetentionAnnotations = includeSourceRetentionAnnotations,
             skipEmitPackages = skipEmitPackages,
             format = format
         )
+        if (checkTextStubEquivalence) {
+            if (stubFilesArr.isEmpty()) {
+                addError("Stub files may not be empty when checkTextStubEquivalence is set to true.")
+                return
+            }
+            if (docStubs) {
+                addError("From-text stub generation is not supported for documentation stub.")
+                return
+            }
+            if (stubFilesArr.any { it !is TestFile.JavaTestFile }) {
+                addError("From-text stub generation is only supported for Java stubs.")
+                return
+            }
+            check(
+                signatureSources = arrayOf(readFile(getApiFile())),
+                showAnnotations = showAnnotations,
+                stubFiles = stubFilesArr,
+                expectedIssues = warnings,
+                checkCompilation = true,
+                extraArguments = arrayOf(*extraArguments, ARG_EXCLUDE_ANNOTATION, ANDROIDX_NONNULL),
+                skipEmitPackages = skipEmitPackages,
+                format = format
+            )
+        }
     }
 
     @Test
@@ -265,7 +292,7 @@
                 package test.pkg;
                 @SuppressWarnings({"unchecked", "deprecation", "all"})
                 public class Foo {
-                private Foo() { throw new RuntimeException("Stub!"); }
+                Foo() { throw new RuntimeException("Stub!"); }
                 }
                 """
         )
@@ -292,7 +319,8 @@
                 public interface Foo {
                 public void foo();
                 }
-                """
+                """,
+            checkTextStubEquivalence = true
         )
     }
 
@@ -420,6 +448,15 @@
                         public static final double field10 = Double.NaN;
                         public static final double field11 = Double.POSITIVE_INFINITY;
 
+                        public static final boolean field12;
+                        public static final byte field13;
+                        public static final char field14;
+                        public static final short field15;
+                        public static final int field16;
+                        public static final long field17;
+                        public static final float field18;
+                        public static final double field19;
+
                         public static final String GOOD_IRI_CHAR = "a-zA-Z0-9\u00a0-\ud7ff\uf900-\ufdcf\ufdf0-\uffef";
                         public static final char HEX_INPUT = 61184;
                     }
@@ -445,8 +482,25 @@
                 public static final java.lang.String field09 = "String with \"escapes\" and \u00a9...";
                 public static final double field10 = (0.0/0.0);
                 public static final double field11 = (1.0/0.0);
+                public static final boolean field12;
+                static { field12 = false; }
+                public static final byte field13;
+                static { field13 = 0; }
+                public static final char field14;
+                static { field14 = 0; }
+                public static final short field15;
+                static { field15 = 0; }
+                public static final int field16;
+                static { field16 = 0; }
+                public static final long field17;
+                static { field17 = 0; }
+                public static final float field18;
+                static { field18 = 0; }
+                public static final double field19;
+                static { field19 = 0; }
                 }
-                """
+                """,
+            checkTextStubEquivalence = true
         )
     }
 
@@ -688,7 +742,7 @@
 
                     @SuppressWarnings("ALL")
                     public class MoreAsserts {
-                        public static void assertEquals(String arg0, Set<? extends Object> arg1, Set<? extends Object> arg2) { }
+                        public static void assertEquals(String arg1, Set<? extends Object> arg2, Set<? extends Object> arg3) { }
                         public static void assertEquals(Set<? extends Object> arg1, Set<? extends Object> arg2) { }
                     }
                     """
@@ -700,10 +754,11 @@
                 @SuppressWarnings({"unchecked", "deprecation", "all"})
                 public class MoreAsserts {
                 public MoreAsserts() { throw new RuntimeException("Stub!"); }
-                public static void assertEquals(java.lang.String arg0, java.util.Set<?> arg1, java.util.Set<?> arg2) { throw new RuntimeException("Stub!"); }
+                public static void assertEquals(java.lang.String arg1, java.util.Set<?> arg2, java.util.Set<?> arg3) { throw new RuntimeException("Stub!"); }
                 public static void assertEquals(java.util.Set<?> arg1, java.util.Set<?> arg2) { throw new RuntimeException("Stub!"); }
                 }
-                """
+                """,
+            checkTextStubEquivalence = true
         )
     }
 
@@ -866,7 +921,8 @@
                     public void method2() { throw new RuntimeException("Stub!"); }
                     public static final java.lang.String CONSTANT = "MyConstant";
                     }
-                """
+                """,
+            checkTextStubEquivalence = true
         )
     }
 
@@ -914,7 +970,8 @@
                 public void other() { throw new RuntimeException("Stub!"); }
                 public static final java.lang.String CONSTANT = "MyConstant";
                 }
-                """
+                """,
+            checkTextStubEquivalence = true
         )
     }
 
@@ -944,7 +1001,8 @@
                 protected void finalize1() throws java.lang.Throwable { throw new RuntimeException("Stub!"); }
                 protected void finalize2() throws java.io.IOException, java.lang.IllegalArgumentException { throw new RuntimeException("Stub!"); }
                 }
-                """
+                """,
+            checkTextStubEquivalence = true
         )
     }
 
@@ -974,7 +1032,8 @@
                 public static final int CONSTANT3 = 0; // 0x0
                 public static final java.lang.String CONSTANT4 = null;
                 }
-                """
+                """,
+            checkTextStubEquivalence = true
         )
     }
 
@@ -993,7 +1052,7 @@
                         }
                         public static final class IsoFields {
                             public static final TemporalField DAY_OF_QUARTER = Field.DAY_OF_QUARTER;
-                            private IsoFields() {
+                            IsoFields() {
                                 throw new AssertionError("Not instantiable");
                             }
 
@@ -1018,7 +1077,7 @@
                     public FinalFieldTest() { throw new RuntimeException("Stub!"); }
                     @SuppressWarnings({"unchecked", "deprecation", "all"})
                     public static final class IsoFields {
-                    private IsoFields() { throw new RuntimeException("Stub!"); }
+                    IsoFields() { throw new RuntimeException("Stub!"); }
                     public static final test.pkg.FinalFieldTest.TemporalField DAY_OF_QUARTER;
                     static { DAY_OF_QUARTER = null; }
                     }
@@ -1111,57 +1170,84 @@
     }
 
     @Test
-    fun `Check generating annotation source`() {
+    fun `Check overridden method added for complex hierarchy`() {
         checkStubs(
             sourceFiles = arrayOf(
                 java(
                     """
-                    package android.view.View;
-                    import android.annotation.IntDef;
-                    import android.annotation.IntRange;
-                    import java.lang.annotation.Retention;
-                    import java.lang.annotation.RetentionPolicy;
-                    public class View {
-                        @SuppressWarnings("all")
-                        public static class MeasureSpec {
-                            private static final int MODE_SHIFT = 30;
-                            private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
-                            /** @hide */
-                            @SuppressWarnings("all")
-                            @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
-                            @Retention(RetentionPolicy.SOURCE)
-                            public @interface MeasureSpecMode {}
-                            public static final int UNSPECIFIED = 0 << MODE_SHIFT;
-                            public static final int EXACTLY     = 1 << MODE_SHIFT;
-                            public static final int AT_MOST     = 2 << MODE_SHIFT;
-
-                            public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
-                                                              @MeasureSpecMode int mode) {
-                                return 0;
-                            }
-                        }
-                    }
-                    """
-                ),
-                intDefAnnotationSource,
-                intRangeAnnotationSource
-            ),
-            warnings = "",
-            source = """
-                    package android.view.View;
-                    @SuppressWarnings({"unchecked", "deprecation", "all"})
-                    public class View {
-                    public View() { throw new RuntimeException("Stub!"); }
-                    @SuppressWarnings({"unchecked", "deprecation", "all"})
-                    public static class MeasureSpec {
-                    public MeasureSpec() { throw new RuntimeException("Stub!"); }
-                    public static int makeMeasureSpec(@androidx.annotation.IntRange(from=0, to=0x40000000 - 1) int size, int mode) { throw new RuntimeException("Stub!"); }
-                    public static final int AT_MOST = -2147483648; // 0x80000000
-                    public static final int EXACTLY = 1073741824; // 0x40000000
-                    public static final int UNSPECIFIED = 0; // 0x0
-                    }
-                    }
+                package test.pkg;
+                public final class A extends C implements B<String> {
+                    @Override public void method2() { }
+                }
                 """
+                ),
+                java(
+                    """
+                package test.pkg;
+                public interface B<T> {
+                    void method1(T arg1);
+                }
+                """
+                ),
+                java(
+                    """
+                package test.pkg;
+                public abstract class C extends D {
+                    public abstract void method2();
+                }
+                """
+                ),
+                java(
+                    """
+                package test.pkg;
+                public abstract class D implements B<String> {
+                    @Override public void method1(String arg1) { }
+                }
+                """
+                )
+            ),
+            stubFiles = arrayOf(
+                java(
+                    """
+                package test.pkg;
+                @SuppressWarnings({"unchecked", "deprecation", "all"})
+                public final class A extends test.pkg.C implements test.pkg.B<java.lang.String> {
+                public A() { throw new RuntimeException("Stub!"); }
+                public void method2() { throw new RuntimeException("Stub!"); }
+                }
+                """
+                ),
+                java(
+                    """
+                package test.pkg;
+                @SuppressWarnings({"unchecked", "deprecation", "all"})
+                public interface B<T> {
+                public void method1(T arg1);
+                }
+                """
+                ),
+                java(
+                    """
+                package test.pkg;
+                @SuppressWarnings({"unchecked", "deprecation", "all"})
+                public abstract class C extends test.pkg.D {
+                public C() { throw new RuntimeException("Stub!"); }
+                public abstract void method2();
+                }
+                """
+                ),
+                java(
+                    """
+                package test.pkg;
+                @SuppressWarnings({"unchecked", "deprecation", "all"})
+                public abstract class D implements test.pkg.B<java.lang.String> {
+                public D() { throw new RuntimeException("Stub!"); }
+                public void method1(java.lang.String arg1) { throw new RuntimeException("Stub!"); }
+                }
+                """
+                )
+            ),
+            checkTextStubEquivalence = true
         )
     }
 
@@ -1192,58 +1278,6 @@
     }
 
     @Test
-    fun `Check generating annotation for hidden constants`() {
-        checkStubs(
-            sourceFiles = arrayOf(
-                java(
-                    """
-                    package test.pkg;
-
-                    import android.content.Intent;
-                    import android.annotation.RequiresPermission;
-
-                    public abstract class HiddenPermission {
-                        @RequiresPermission(allOf = {
-                                android.Manifest.permission.INTERACT_ACROSS_USERS,
-                                android.Manifest.permission.BROADCAST_STICKY
-                        })
-                        public abstract void removeStickyBroadcast(@RequiresPermission Object intent);
-                    }
-                    """
-                ),
-                java(
-                    """
-                    package android;
-
-                    public final class Manifest {
-                        @SuppressWarnings("JavaDoc")
-                        public static final class permission {
-                            public static final String BROADCAST_STICKY = "android.permission.BROADCAST_STICKY";
-                            /** @SystemApi @hide Allows an application to call APIs that allow it to do interactions
-                             across the users on the device, using singleton services and
-                             user-targeted broadcasts.  This permission is not available to
-                             third party applications. */
-                            public static final String INTERACT_ACROSS_USERS = "android.permission.INTERACT_ACROSS_USERS";
-                        }
-                    }
-                    """
-                ),
-                requiresPermissionSource
-            ),
-            warnings = "",
-            source = """
-                    package test.pkg;
-                    @SuppressWarnings({"unchecked", "deprecation", "all"})
-                    public abstract class HiddenPermission {
-                    public HiddenPermission() { throw new RuntimeException("Stub!"); }
-                    @androidx.annotation.RequiresPermission(allOf={"android.permission.INTERACT_ACROSS_USERS", android.Manifest.permission.BROADCAST_STICKY})
-                    public abstract void removeStickyBroadcast(@androidx.annotation.RequiresPermission java.lang.Object intent);
-                    }
-                """
-        )
-    }
-
-    @Test
     fun `Check generating type parameters in interface list`() {
         checkStubs(
             format = FileFormat.V2,
@@ -1502,35 +1536,35 @@
                     @SuppressWarnings("WeakerAccess")
                     public class Constructors {
                         public class Parent {
-                            public Parent(String s, int i, long l, boolean b, short sh) {
+                            public Parent(String arg1, int arg2, long arg3, boolean arg4, short arg5) {
                             }
                         }
 
                         public class Child extends Parent {
-                            public Child(String s, int i, long l, boolean b, short sh) {
-                                super(s, i, l, b, sh);
+                            public Child(String arg1, int arg2, long arg3, boolean arg4, short arg5) {
+                                super(arg1, arg2, arg3, arg4, arg5);
                             }
 
-                            private Child(String s) {
-                                super(s, 0, 0, false, 0);
+                            private Child(String arg1) {
+                                super(arg1, 0, 0, false, 0);
                             }
                         }
 
                         public class Child2 extends Parent {
-                            Child2(String s) {
-                                super(s, 0, 0, false, 0);
+                            Child2(String arg1) {
+                                super(arg1, 0, 0, false, 0);
                             }
                         }
 
                         public class Child3 extends Child2 {
-                            private Child3(String s) {
+                            private Child3(String arg1) {
                                 super("something");
                             }
                         }
 
                         public class Child4 extends Parent {
-                            Child4(String s, HiddenClass hidden) {
-                                super(s, 0, 0, true, 0);
+                            Child4(String arg1, HiddenClass arg2) {
+                                super(arg1, 0, 0, true, 0);
                             }
                         }
                         /** @hide */
@@ -1547,7 +1581,7 @@
                     public Constructors() { throw new RuntimeException("Stub!"); }
                     @SuppressWarnings({"unchecked", "deprecation", "all"})
                     public class Child extends test.pkg.Constructors.Parent {
-                    public Child(java.lang.String s, int i, long l, boolean b, short sh) { super(null, 0, 0, false, (short)0); throw new RuntimeException("Stub!"); }
+                    public Child(java.lang.String arg1, int arg2, long arg3, boolean arg4, short arg5) { super(null, 0, 0, false, (short)0); throw new RuntimeException("Stub!"); }
                     }
                     @SuppressWarnings({"unchecked", "deprecation", "all"})
                     public class Child2 extends test.pkg.Constructors.Parent {
@@ -1555,7 +1589,7 @@
                     }
                     @SuppressWarnings({"unchecked", "deprecation", "all"})
                     public class Child3 extends test.pkg.Constructors.Child2 {
-                    private Child3() { throw new RuntimeException("Stub!"); }
+                    Child3() { throw new RuntimeException("Stub!"); }
                     }
                     @SuppressWarnings({"unchecked", "deprecation", "all"})
                     public class Child4 extends test.pkg.Constructors.Parent {
@@ -1563,10 +1597,11 @@
                     }
                     @SuppressWarnings({"unchecked", "deprecation", "all"})
                     public class Parent {
-                    public Parent(java.lang.String s, int i, long l, boolean b, short sh) { throw new RuntimeException("Stub!"); }
+                    public Parent(java.lang.String arg1, int arg2, long arg3, boolean arg4, short arg5) { throw new RuntimeException("Stub!"); }
                     }
                     }
-                    """
+                    """,
+            checkTextStubEquivalence = true
         )
     }
 
@@ -1636,7 +1671,7 @@
                     }
                     @SuppressWarnings({"unchecked", "deprecation", "all"})
                     public class Child3 extends test.pkg.Constructors.Child2 {
-                    private Child3() { throw new RuntimeException("Stub!"); }
+                    Child3() { throw new RuntimeException("Stub!"); }
                     }
                     @SuppressWarnings({"unchecked", "deprecation", "all"})
                     public class Child4 extends test.pkg.Constructors.Parent {
@@ -1862,6 +1897,53 @@
     }
 
     @Test
+    fun `Check resolving override equivalent signatures`() {
+        // getAttributeNamespace in XmlResourceParser does not exist in the intermediate text file created.
+        checkStubs(
+            sourceFiles = arrayOf(
+                java(
+                    """
+                    package test.pkg;
+                    public interface XmlResourceParser extends test.pkg.XmlPullParser, test.pkg.AttributeSet {
+                        public void close();
+                        String getAttributeNamespace (int arg1);
+                    }
+                    """
+                ),
+                java(
+                    """
+                    package test.pkg;
+                    public interface XmlPullParser {
+                        String getAttributeNamespace (int arg1);
+                    }
+                    """
+                ),
+                java(
+                    """
+                    package test.pkg;
+                    public interface AttributeSet {
+                        default String getAttributeNamespace (int arg1) { }
+                    }
+                    """
+                )
+            ),
+            stubFiles = arrayOf(
+                java(
+                    """
+                    package test.pkg;
+                    @SuppressWarnings({"unchecked", "deprecation", "all"})
+                    public interface XmlResourceParser extends test.pkg.XmlPullParser,  test.pkg.AttributeSet {
+                    public void close();
+                    public java.lang.String getAttributeNamespace(int arg1);
+                    }
+                    """
+                )
+            ),
+            checkTextStubEquivalence = true
+        )
+    }
+
+    @Test
     fun `Rewrite unknown nullability annotations as sdk stubs`() {
         check(
             format = FileFormat.V2,
@@ -2595,7 +2677,8 @@
                     public static interface TypeEvaluator<T> {
                     }
                     }
-                    """
+                    """,
+            checkTextStubEquivalence = true
         )
     }
 
@@ -3354,7 +3437,6 @@
     @Test
     fun `Annotation metadata in stubs`() {
         checkStubs(
-            includeSourceRetentionAnnotations = false,
             skipEmitPackages = emptyList(),
             sourceFiles = arrayOf(
                 java(
@@ -3406,7 +3488,8 @@
                 public interface MyInterface {
                 public void run();
                 }
-                """
+                """,
+            checkTextStubEquivalence = true
         )
     }
 
@@ -3490,7 +3573,8 @@
                     }
                     """
                 )
-            )
+            ),
+            docStubs = true
         )
     }
 
@@ -3520,7 +3604,6 @@
             stubFiles = arrayOf(
                 java(
                     """
-                    @androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.SUBCLASSES)
                     package test.pkg;
                     """
                 ),
@@ -3635,7 +3718,7 @@
                     @Deprecated
                     @test.pkg.MyRuntimeRetentionAnnotation
                     public class Foo {
-                    private Foo() { throw new RuntimeException("Stub!"); }
+                    Foo() { throw new RuntimeException("Stub!"); }
                     }
                     """
                 )
@@ -3704,7 +3787,7 @@
                     @test.pkg.MyClassRetentionAnnotation
                     @test.pkg.MyRuntimeRetentionAnnotation
                     public class Foo {
-                    private Foo() { throw new RuntimeException("Stub!"); }
+                    Foo() { throw new RuntimeException("Stub!"); }
                     @Deprecated
                     public void bar() { throw new RuntimeException("Stub!"); }
                     @Deprecated protected int foo;
@@ -4256,7 +4339,7 @@
                     package test.pkg;
                     @SuppressWarnings({"unchecked", "deprecation", "all"})
                     public class Orange {
-                    private Orange() { throw new RuntimeException("Stub!"); }
+                    Orange() { throw new RuntimeException("Stub!"); }
                     }
                     """
                 ),
@@ -4265,7 +4348,7 @@
                     package test.pkg;
                     @SuppressWarnings({"unchecked", "deprecation", "all"})
                     public class Alpha extends test.pkg.Charlie<test.pkg.Orange> {
-                    private Alpha() { throw new RuntimeException("Stub!"); }
+                    Alpha() { throw new RuntimeException("Stub!"); }
                     }
                     """
                 )
@@ -4588,7 +4671,7 @@
                     package test.pkg;
                     @SuppressWarnings({"unchecked", "deprecation", "all"})
                     public class PublicApi {
-                    private PublicApi() { throw new RuntimeException("Stub!"); }
+                    PublicApi() { throw new RuntimeException("Stub!"); }
                     /** @deprecated My deprecation reason 1 */
                     @Deprecated
                     public static void method1() { throw new RuntimeException("Stub!"); }
@@ -4697,7 +4780,7 @@
                     package test.pkg;
                     @SuppressWarnings({"unchecked", "deprecation", "all"})
                     public class PublicApi2 {
-                    private PublicApi2() { throw new RuntimeException("Stub!"); }
+                    PublicApi2() { throw new RuntimeException("Stub!"); }
                     public static void method1() { throw new RuntimeException("Stub!"); }
                     /**
                      * My docs.
diff --git a/src/testdata/prebuilts-sdk-test/30/ClassAddedInApi30.java b/src/testdata/prebuilts-sdk-test/30/ClassAddedInApi30.java
new file mode 100644
index 0000000..50ea924
--- /dev/null
+++ b/src/testdata/prebuilts-sdk-test/30/ClassAddedInApi30.java
@@ -0,0 +1,6 @@
+package android.test;
+
+public class ClassAddedInApi30 {
+    private ClassAddedInApi30() {}
+    public void methodAddedInApi30() { throw new RuntimeException("Stub!"); }
+}
diff --git a/src/testdata/prebuilts-sdk-test/30/Object.java b/src/testdata/prebuilts-sdk-test/30/Object.java
new file mode 100644
index 0000000..a673c1a
--- /dev/null
+++ b/src/testdata/prebuilts-sdk-test/30/Object.java
@@ -0,0 +1,4 @@
+package java.lang;
+
+public class Object {
+}
diff --git a/src/testdata/prebuilts-sdk-test/31/ClassAddedInApi30.java b/src/testdata/prebuilts-sdk-test/31/ClassAddedInApi30.java
new file mode 100644
index 0000000..9cb6703
--- /dev/null
+++ b/src/testdata/prebuilts-sdk-test/31/ClassAddedInApi30.java
@@ -0,0 +1,7 @@
+package android.test;
+
+public class ClassAddedInApi30 {
+    private ClassAddedInApi30() {}
+    public void methodAddedInApi30() { throw new RuntimeException("Stub!"); }
+    public void methodAddedInApi31() { throw new RuntimeException("Stub!"); }
+}
diff --git a/src/testdata/prebuilts-sdk-test/31/ClassAddedInApi31AndExt2.java b/src/testdata/prebuilts-sdk-test/31/ClassAddedInApi31AndExt2.java
new file mode 100644
index 0000000..772b830
--- /dev/null
+++ b/src/testdata/prebuilts-sdk-test/31/ClassAddedInApi31AndExt2.java
@@ -0,0 +1,7 @@
+package android.test;
+
+public class ClassAddedInApi31AndExt2 {
+    private ClassAddedInApi31AndExt2() {}
+    public static final int FIELD_ADDED_IN_API_31_AND_EXT_2 = 1;
+    public void methodAddedInApi31AndExt2() { throw new RuntimeException("Stub!"); }
+}
diff --git a/src/testdata/prebuilts-sdk-test/31/ClassAddedInExt1.java b/src/testdata/prebuilts-sdk-test/31/ClassAddedInExt1.java
new file mode 100644
index 0000000..cb9c864
--- /dev/null
+++ b/src/testdata/prebuilts-sdk-test/31/ClassAddedInExt1.java
@@ -0,0 +1,9 @@
+package android.test;
+
+public class ClassAddedInExt1 {
+    private ClassAddedInExt1() {}
+    public static final int FIELD_ADDED_IN_EXT_1 = 1;
+    public static final int FIELD_ADDED_IN_API_31_AND_EXT_2 = 2;
+    public void methodAddedInExt1() { throw new RuntimeException("Stub!"); }
+    public void methodAddedInApi31AndExt2() { throw new RuntimeException("Stub!"); }
+}
diff --git a/src/testdata/prebuilts-sdk-test/31/Object.java b/src/testdata/prebuilts-sdk-test/31/Object.java
new file mode 100644
index 0000000..a673c1a
--- /dev/null
+++ b/src/testdata/prebuilts-sdk-test/31/Object.java
@@ -0,0 +1,4 @@
+package java.lang;
+
+public class Object {
+}
diff --git a/src/testdata/prebuilts-sdk-test/README.md b/src/testdata/prebuilts-sdk-test/README.md
new file mode 100644
index 0000000..3e8cf72
--- /dev/null
+++ b/src/testdata/prebuilts-sdk-test/README.md
@@ -0,0 +1,11 @@
+Sources for building a prebuilts/sdk tree for metalava use in unit tests.
+
+The directory tree and generated jar files will be build as part of the metalava
+unit tests, and placed in $(gettop)/out/metalava/prebuilts/sdk.
+
+The project emulates the following history, in order of earliest to latest event:
+
+- finalized Android API 30 "Android R" (android-30)
+- finalized SDK extensions "R-ext" version 1 (android-30-ext1)
+- finalized Android API 31 "Android S" and SDK extensions "S-ext", version 2, at the same time (android-31-ext2)
+- finalized SDK extensions "S-ext", version 3 (android-31-ext3)
diff --git a/src/testdata/prebuilts-sdk-test/extensions/1/ClassAddedInExt1.java b/src/testdata/prebuilts-sdk-test/extensions/1/ClassAddedInExt1.java
new file mode 100644
index 0000000..aef7e0a
--- /dev/null
+++ b/src/testdata/prebuilts-sdk-test/extensions/1/ClassAddedInExt1.java
@@ -0,0 +1,7 @@
+package android.test;
+
+public class ClassAddedInExt1 {
+    private ClassAddedInExt1() {}
+    public static final int FIELD_ADDED_IN_EXT_1 = 1;
+    public void methodAddedInExt1() { throw new RuntimeException("Stub!"); }
+}
diff --git a/src/testdata/prebuilts-sdk-test/extensions/2/ClassAddedInApi31AndExt2.java b/src/testdata/prebuilts-sdk-test/extensions/2/ClassAddedInApi31AndExt2.java
new file mode 120000
index 0000000..1d7d592
--- /dev/null
+++ b/src/testdata/prebuilts-sdk-test/extensions/2/ClassAddedInApi31AndExt2.java
@@ -0,0 +1 @@
+../../31/ClassAddedInApi31AndExt2.java
\ No newline at end of file
diff --git a/src/testdata/prebuilts-sdk-test/extensions/2/ClassAddedInExt1.java b/src/testdata/prebuilts-sdk-test/extensions/2/ClassAddedInExt1.java
new file mode 120000
index 0000000..cb0c486
--- /dev/null
+++ b/src/testdata/prebuilts-sdk-test/extensions/2/ClassAddedInExt1.java
@@ -0,0 +1 @@
+../../31/ClassAddedInExt1.java
\ No newline at end of file
diff --git a/src/testdata/prebuilts-sdk-test/extensions/3/ClassAddedInApi31AndExt2.java b/src/testdata/prebuilts-sdk-test/extensions/3/ClassAddedInApi31AndExt2.java
new file mode 100644
index 0000000..fa31b8e
--- /dev/null
+++ b/src/testdata/prebuilts-sdk-test/extensions/3/ClassAddedInApi31AndExt2.java
@@ -0,0 +1,9 @@
+package android.test;
+
+public class ClassAddedInApi31AndExt2 {
+    private ClassAddedInApi31AndExt2() {}
+    public static final int FIELD_ADDED_IN_API_31_AND_EXT_2 = 1;
+    public static final int FIELD_ADDED_IN_EXT_3 = 2;
+    public void methodAddedInApi31AndExt2() { throw new RuntimeException("Stub!"); }
+    public void methodAddedInExt3() { throw new RuntimeException("Stub!"); }
+}
diff --git a/src/testdata/prebuilts-sdk-test/extensions/3/ClassAddedInExt1.java b/src/testdata/prebuilts-sdk-test/extensions/3/ClassAddedInExt1.java
new file mode 100644
index 0000000..c5798d6
--- /dev/null
+++ b/src/testdata/prebuilts-sdk-test/extensions/3/ClassAddedInExt1.java
@@ -0,0 +1,11 @@
+package android.test;
+
+public class ClassAddedInExt1 {
+    private ClassAddedInExt1() {}
+    public static final int FIELD_ADDED_IN_EXT_1 = 1;
+    public static final int FIELD_ADDED_IN_API_31_AND_EXT_2 = 2;
+    public static final int FIELD_ADDED_IN_EXT_3 = 3;
+    public void methodAddedInExt1() { throw new RuntimeException("Stub!"); }
+    public void methodAddedInApi31AndExt2() { throw new RuntimeException("Stub!"); }
+    public void methodAddedInExt3() { throw new RuntimeException("Stub!"); }
+}
diff --git a/src/testdata/prebuilts-sdk-test/extensions/3/ClassAddedInExt3.java b/src/testdata/prebuilts-sdk-test/extensions/3/ClassAddedInExt3.java
new file mode 100644
index 0000000..31b58ec
--- /dev/null
+++ b/src/testdata/prebuilts-sdk-test/extensions/3/ClassAddedInExt3.java
@@ -0,0 +1,7 @@
+package android.test;
+
+public class ClassAddedInExt3 {
+    private ClassAddedInExt3() {}
+    public static final int FIELD_ADDED_IN_EXT_3 = 1;
+    public void methodAddedInExt3() { throw new RuntimeException("Stub!"); }
+}
diff --git a/src/testdata/prebuilts-sdk-test/sdk-extensions-info.xml b/src/testdata/prebuilts-sdk-test/sdk-extensions-info.xml
new file mode 100644
index 0000000..a7598e9
--- /dev/null
+++ b/src/testdata/prebuilts-sdk-test/sdk-extensions-info.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2022 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.
+-->
+
+<sdk-extensions-info>
+  <!-- SDK definitions -->
+  <sdk
+    id="30"
+    shortname="R-ext"
+    name="R Extensions"
+    reference="android/os/Build$VERSION_CODES$R" />
+  <sdk
+    id="31"
+    shortname="S-ext"
+    name="S Extensions"
+    reference="android/os/Build$VERSION_CODES$S" />
+
+  <!-- TEST EXTENSION -->
+  <symbol
+    jar="framework-ext"
+    pattern="*"
+    sdks="R-ext,S-ext" />
+</sdk-extensions-info>