diff --git a/.idea/dictionaries/metalava.xml b/.idea/dictionaries/metalava.xml
index bf8e8982a93e77ab643f62705c6437073726c30e..6b14ab559968878c961737867ca04abb40564b1f 100644
--- a/.idea/dictionaries/metalava.xml
+++ b/.idea/dictionaries/metalava.xml
@@ -1,6 +1,7 @@
 <component name="ProjectDictionaryState">
   <dictionary name="metalava">
     <words>
+      <w>androidx</w>
       <w>apidocsdir</w>
       <w>argnum</w>
       <w>bootclasspath</w>
@@ -23,6 +24,7 @@
       <w>includeable</w>
       <w>inheritdoc</w>
       <w>interop</w>
+      <w>jaif</w>
       <w>javadocs</w>
       <w>jvmstatic</w>
       <w>knowntags</w>
diff --git a/.idea/dictionaries/tnorbye.xml b/.idea/dictionaries/tnorbye.xml
deleted file mode 100644
index c86b073dd697bf7ac05393727bdb780034c17751..0000000000000000000000000000000000000000
--- a/.idea/dictionaries/tnorbye.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-<component name="ProjectDictionaryState">
-  <dictionary name="tnorbye">
-    <words>
-      <w>androidx</w>
-    </words>
-  </dictionary>
-</component>
\ No newline at end of file
diff --git a/src/main/java/com/android/tools/metalava/AnnotationsMerger.kt b/src/main/java/com/android/tools/metalava/AnnotationsMerger.kt
index 97b8cf3739cec1b7235b98438fe054c481909eb8..f321ce9170875ef40190422fa7c6601b453c4289 100644
--- a/src/main/java/com/android/tools/metalava/AnnotationsMerger.kt
+++ b/src/main/java/com/android/tools/metalava/AnnotationsMerger.kt
@@ -31,7 +31,6 @@ import com.android.SdkConstants.STRING_DEF_ANNOTATION
 import com.android.SdkConstants.TYPE_DEF_FLAG_ATTRIBUTE
 import com.android.SdkConstants.TYPE_DEF_VALUE_ATTRIBUTE
 import com.android.SdkConstants.VALUE_TRUE
-import com.android.annotations.NonNull
 import com.android.tools.lint.annotations.Extractor.ANDROID_INT_DEF
 import com.android.tools.lint.annotations.Extractor.ANDROID_NOTNULL
 import com.android.tools.lint.annotations.Extractor.ANDROID_NULLABLE
@@ -54,6 +53,7 @@ import com.android.tools.metalava.model.Codebase
 import com.android.tools.metalava.model.DefaultAnnotationValue
 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.psi.PsiAnnotationItem
 import com.android.tools.metalava.model.visitors.ApiVisitor
 import com.android.utils.XmlUtils
@@ -80,7 +80,7 @@ class AnnotationsMerger(
         mergeAnnotations.forEach { mergeExisting(it) }
     }
 
-    private fun mergeExisting(@NonNull file: File) {
+    private fun mergeExisting(file: File) {
         if (file.isDirectory) {
             val files = file.listFiles()
             if (files != null) {
@@ -98,11 +98,18 @@ class AnnotationsMerger(
                 } catch (e: IOException) {
                     error("Aborting: I/O problem during transform: " + e.toString())
                 }
+            } else if (file.path.endsWith(".jaif")) {
+                try {
+                    val jaif = Files.asCharSource(file, Charsets.UTF_8).read()
+                    mergeAnnotationsJaif(file.path, jaif)
+                } catch (e: IOException) {
+                    error("Aborting: I/O problem during transform: " + e.toString())
+                }
             }
         }
     }
 
-    private fun mergeFromJar(@NonNull jar: File) {
+    private fun mergeFromJar(jar: File) {
         // Reads in an existing annotations jar and merges in entries found there
         // with the annotations analyzed from source.
         var zis: JarInputStream? = null
@@ -129,7 +136,7 @@ class AnnotationsMerger(
         }
     }
 
-    private fun mergeAnnotationsXml(@NonNull path: String, @NonNull xml: String) {
+    private fun mergeAnnotationsXml(path: String, xml: String) {
         try {
             val document = XmlUtils.parseDocument(xml, false)
             mergeDocument(document)
@@ -145,6 +152,103 @@ class AnnotationsMerger(
         }
     }
 
+    private fun mergeAnnotationsJaif(path: String, jaif: String) {
+        var pkgItem: PackageItem? = null
+        var clsItem: ClassItem? = null
+        var methodItem: MethodItem? = null
+        var curr: Item? = null
+
+        for (rawLine in jaif.split("\n")) {
+            val line = rawLine.trim()
+            if (line.isEmpty()) {
+                continue
+            }
+            if (line.startsWith("//")) {
+                continue
+            }
+            if (line.startsWith("package ")) {
+                val pkg = line.substring("package ".length, line.length - 1)
+                pkgItem = codebase.findPackage(pkg)
+                curr = pkgItem
+            } else if (line.startsWith("class ")) {
+                val cls = line.substring("class ".length, line.length - 1)
+                clsItem = if (pkgItem != null)
+                    codebase.findClass(pkgItem.qualifiedName() + "." + cls)
+                else
+                    null
+                curr = clsItem
+            } else if (line.startsWith("annotation ")) {
+                val cls = line.substring("annotation ".length, line.length - 1)
+                clsItem = if (pkgItem != null)
+                    codebase.findClass(pkgItem.qualifiedName() + "." + cls)
+                else
+                    null
+                curr = clsItem
+            } else if (line.startsWith("method ")) {
+                val method = line.substring("method ".length, line.length - 1)
+                methodItem = null
+                if (clsItem != null) {
+                    val index = method.indexOf('(')
+                    if (index != -1) {
+                        val name = method.substring(0, index)
+                        val desc = method.substring(index)
+                        methodItem = clsItem.findMethodByDesc(name, desc, true, true)
+                    }
+                }
+                curr = methodItem
+            } else if (line.startsWith("field ")) {
+                val field = line.substring("field ".length, line.length - 1)
+                val fieldItem = clsItem?.findField(field, true, true)
+                curr = fieldItem
+            } else if (line.startsWith("parameter #")) {
+                val parameterIndex = line.substring("parameter #".length, line.length - 1).toInt()
+                val parameterItem = if (methodItem != null) {
+                    methodItem.parameters()[parameterIndex]
+                } else {
+                    null
+                }
+                curr = parameterItem
+            } else if (line.startsWith("type: ")) {
+                val typeAnnotation = line.substring("type: ".length)
+                if (curr != null) {
+                    mergeJaifAnnotation(path, curr, typeAnnotation)
+                }
+            } else if (line.startsWith("return: ")) {
+                val annotation = line.substring("return: ".length)
+                if (methodItem != null) {
+                    mergeJaifAnnotation(path, methodItem, annotation)
+                }
+            } else if (line.startsWith("inner-type")) {
+                warning("$path: Skipping inner-type annotations for now ($line)")
+            } else if (line.startsWith("int ")) {
+                // warning("Skipping int attribute definitions for annotations now ($line)")
+            }
+        }
+    }
+
+    private fun mergeJaifAnnotation(
+        path: String,
+        item: Item,
+        annotationSource: String
+    ) {
+        if (annotationSource.isEmpty()) {
+            return
+        }
+
+        if (annotationSource.contains("(")) {
+            warning("$path: Can't merge complex annotations from jaif yet: $annotationSource")
+            return
+        }
+        val originalName = annotationSource.substring(1) // remove "@"
+        val qualifiedName = AnnotationItem.mapName(codebase, originalName) ?: originalName
+        if (hasNullnessConflicts(item, qualifiedName)) {
+            return
+        }
+
+        val annotationItem = codebase.createAnnotation("@$qualifiedName")
+        item.mutableModifiers().addAnnotation(annotationItem)
+    }
+
     internal fun error(message: String) {
         // TODO: Integrate with metalava error facility
         options.stderr.println("Error: $message")
@@ -163,7 +267,7 @@ class AnnotationsMerger(
         "(\\S+) (\\S+|((.*)\\s+)?(\\S+)\\((.*)\\)( \\d+)?)"
     )
 
-    private fun mergeDocument(@NonNull document: Document) {
+    private fun mergeDocument(document: Document) {
 
         val root = document.documentElement
         val rootTag = root.tagName
@@ -324,39 +428,44 @@ class AnnotationsMerger(
         return qualifiedName
     }
 
-    private fun mergeAnnotations(xmlElement: Element, item: Item): Int {
-        var count = 0
-
+    private fun mergeAnnotations(xmlElement: Element, item: Item) {
         loop@ for (annotationElement in getChildren(xmlElement)) {
             val originalName = getAnnotationName(annotationElement)
             val qualifiedName = AnnotationItem.mapName(codebase, originalName) ?: originalName
-            var haveNullable = false
-            var haveNotNull = false
-            for (existing in item.modifiers.annotations()) {
-                val name = existing.qualifiedName() ?: continue
-                if (isNonNull(name)) {
-                    haveNotNull = true
-                }
-                if (isNullable(name)) {
-                    haveNullable = true
-                }
-                if (name == qualifiedName) {
-                    continue@loop
-                }
-            }
-
-            // Make sure we don't have a conflict between nullable and not nullable
-            if (isNonNull(qualifiedName) && haveNullable || isNullable(qualifiedName) && haveNotNull) {
-                warning("Found both @Nullable and @NonNull after import for $item")
-                continue
+            if (hasNullnessConflicts(item, qualifiedName)) {
+                continue@loop
             }
 
             val annotationItem = createAnnotation(annotationElement) ?: continue
             item.mutableModifiers().addAnnotation(annotationItem)
-            count++
         }
+    }
 
-        return count
+    private fun hasNullnessConflicts(
+        item: Item,
+        qualifiedName: String
+    ): Boolean {
+        var haveNullable = false
+        var haveNotNull = false
+        for (existing in item.modifiers.annotations()) {
+            val name = existing.qualifiedName() ?: continue
+            if (isNonNull(name)) {
+                haveNotNull = true
+            }
+            if (isNullable(name)) {
+                haveNullable = true
+            }
+            if (name == qualifiedName) {
+                return true
+            }
+        }
+
+        // Make sure we don't have a conflict between nullable and not nullable
+        if (isNonNull(qualifiedName) && haveNullable || isNullable(qualifiedName) && haveNotNull) {
+            warning("Found both @Nullable and @NonNull after import for $item")
+            return true
+        }
+        return false
     }
 
     /** Reads in annotation data from an XML item (using IntelliJ IDE's external annotations XML format) and
@@ -561,8 +670,7 @@ class AnnotationsMerger(
             name == SUPPORT_NULLABLE
     }
 
-    @NonNull
-    private fun unescapeXml(@NonNull escaped: String): String {
+    private fun unescapeXml(escaped: String): String {
         var workingString = escaped.replace(QUOT_ENTITY, "\"")
         workingString = workingString.replace(LT_ENTITY, "<")
         workingString = workingString.replace(GT_ENTITY, ">")
diff --git a/src/main/java/com/android/tools/metalava/Driver.kt b/src/main/java/com/android/tools/metalava/Driver.kt
index 20a0c3d30c05e9a83aae55fdcb76f4905c7851a0..820d69445ab2facf95a2559d4eec83effe484401 100644
--- a/src/main/java/com/android/tools/metalava/Driver.kt
+++ b/src/main/java/com/android/tools/metalava/Driver.kt
@@ -185,6 +185,23 @@ private fun processFlags() {
     // Generate the documentation stubs *before* we migrate nullness information.
     options.docStubsDir?.let { createStubFiles(it, codebase, docStubs = true, writeStubList = true) }
 
+    val currentApiFile = options.currentApi
+    if (currentApiFile != null && options.checkCompatibility) {
+        val current =
+            if (currentApiFile.path.endsWith(SdkConstants.DOT_JAR)) {
+                loadFromJarFile(currentApiFile)
+            } else {
+                loadFromSignatureFiles(
+                    currentApiFile, options.inputKotlinStyleNulls,
+                    supportsStagedNullability = true
+                )
+            }
+
+        // If configured, compares the new API with the previous API and reports
+        // any incompatibilities.
+        CompatibilityCheck.checkCompatibility(codebase, current)
+    }
+
     val previousApiFile = options.previousApi
     if (previousApiFile != null) {
         val previous =
@@ -199,7 +216,7 @@ private fun processFlags() {
 
         // If configured, compares the new API with the previous API and reports
         // any incompatibilities.
-        if (options.checkCompatibility) {
+        if (options.checkCompatibility && options.currentApi == null) { // otherwise checked against currentApi above
             CompatibilityCheck.checkCompatibility(codebase, previous)
         }
 
@@ -224,6 +241,17 @@ private fun processFlags() {
         }
     }
 
+    options.dexApiFile?.let { apiFile ->
+        val apiFilter = FilterPredicate(ApiPredicate(codebase))
+        val memberIsNotCloned: Predicate<Item> = Predicate { !it.isCloned() }
+        val apiReference = ApiPredicate(codebase, ignoreShown = true)
+        val dexApiEmit = memberIsNotCloned.and(apiFilter)
+
+        createReportFile(
+            codebase, apiFile, "DEX API"
+        ) { printWriter -> DexApiWriter(printWriter, dexApiEmit, apiReference) }
+    }
+
     options.removedApiFile?.let { apiFile ->
         val unfiltered = codebase.original ?: codebase
 
@@ -543,7 +571,6 @@ private fun ensurePsiFileCapacity() {
 
 private fun extractAnnotations(codebase: Codebase, file: File) {
     val localTimer = Stopwatch.createStarted()
-    val units = codebase.units
 
     options.externalAnnotations?.let { outputFile ->
         @Suppress("UNCHECKED_CAST")
diff --git a/src/main/java/com/android/tools/metalava/KotlinInteropChecks.kt b/src/main/java/com/android/tools/metalava/KotlinInteropChecks.kt
index e7b621c96d3fbf62c207d32191e3c4154dfd8695..7af08d1cf3deeb0e91e1201a4eca58650e0f18d7 100644
--- a/src/main/java/com/android/tools/metalava/KotlinInteropChecks.kt
+++ b/src/main/java/com/android/tools/metalava/KotlinInteropChecks.kt
@@ -16,7 +16,6 @@
 
 package com.android.tools.metalava
 
-import com.android.annotations.NonNull
 import com.android.tools.metalava.doclava1.Errors
 import com.android.tools.metalava.model.Codebase
 import com.android.tools.metalava.model.FieldItem
@@ -346,7 +345,7 @@ class KotlinInteropChecks {
     }
 
     /** Returns true if the given string is a reserved Java keyword  */
-    fun isJavaKeyword(@NonNull keyword: String): Boolean {
+    fun isJavaKeyword(keyword: String): Boolean {
         // TODO when we built on top of IDEA core replace this with
         //   JavaLexer.isKeyword(candidate, LanguageLevel.JDK_1_5)
         when (keyword) {
diff --git a/src/main/java/com/android/tools/metalava/Options.kt b/src/main/java/com/android/tools/metalava/Options.kt
index 1d1c8104f136866ad3248eedb365a9976482dd78..3f4df5928693bfe05c1b228adf8c4b657f669a47 100644
--- a/src/main/java/com/android/tools/metalava/Options.kt
+++ b/src/main/java/com/android/tools/metalava/Options.kt
@@ -47,6 +47,7 @@ private const val ARG_SOURCE_PATH = "--source-path"
 private const val ARG_SOURCE_FILES = "--source-files"
 private const val ARG_API = "--api"
 private const val ARG_PRIVATE_API = "--private-api"
+private const val ARG_DEX_API = "--dex-api"
 private const val ARG_PRIVATE_DEX_API = "--private-dex-api"
 private const val ARG_SDK_VALUES = "--sdk-values"
 private const val ARG_REMOVED_API = "--removed-api"
@@ -63,6 +64,7 @@ private const val ARG_EXCLUDE_ANNOTATIONS = "--exclude-annotations"
 private const val ARG_HIDE_PACKAGE = "--hide-package"
 private const val ARG_MANIFEST = "--manifest"
 private const val ARG_PREVIOUS_API = "--previous-api"
+private const val ARG_CURRENT_API = "--current-api"
 private const val ARG_MIGRATE_NULLNESS = "--migrate-nullness"
 private const val ARG_CHECK_COMPATIBILITY = "--check-compatibility"
 private const val ARG_INPUT_KOTLIN_NULLS = "--input-kotlin-nulls"
@@ -239,6 +241,9 @@ class Options(
     /** If set, a file to write the private API file to. Corresponds to the --private-api/-privateApi flag. */
     var privateApiFile: File? = null
 
+    /** If set, a file to write the DEX signatures to. Corresponds to --dex-api. */
+    var dexApiFile: File? = null
+
     /** If set, a file to write the private DEX signatures to. Corresponds to --private-dex-api. */
     var privateDexApiFile: File? = null
 
@@ -267,11 +272,16 @@ class Options(
     var generateAnnotations = true
 
     /**
-     * A signature file for the previous version of this API (for compatibility checks, nullness
-     * migration, etc.)
+     * A signature file for the previous version of this API (for nullness
+     * migration, possibly for compatibility checking (if [currentApi] is not defined), etc.)
      */
     var previousApi: File? = null
 
+    /**
+     * A signature file for the current version of this API (for compatibility checks).
+     */
+    var currentApi: File? = null
+
     /** Whether we should check API compatibility based on the previous API in [previousApi] */
     var checkCompatibility: Boolean = false
 
@@ -373,7 +383,6 @@ class Options(
         stdout.println()
         stdout.flush()
 
-        val apiFilters = mutableListOf<File>()
         var androidJarPatterns: MutableList<String>? = null
         var currentCodeName: String? = null
         var currentJar: File? = null
@@ -431,6 +440,7 @@ class Options(
                 "-sdkvalues", ARG_SDK_VALUES -> sdkValueDir = stringToNewDir(getValue(args, ++index))
 
                 ARG_API, "-api" -> apiFile = stringToNewFile(getValue(args, ++index))
+                ARG_DEX_API, "-dexApi" -> dexApiFile = stringToNewFile(getValue(args, ++index))
 
                 ARG_PRIVATE_API, "-privateApi" -> privateApiFile = stringToNewFile(getValue(args, ++index))
                 ARG_PRIVATE_DEX_API, "-privateDexApi" -> privateDexApiFile = stringToNewFile(getValue(args, ++index))
@@ -509,6 +519,7 @@ class Options(
                 ARG_EXTRACT_ANNOTATIONS -> externalAnnotations = stringToNewFile(getValue(args, ++index))
 
                 ARG_PREVIOUS_API -> previousApi = stringToExistingFile(getValue(args, ++index))
+                ARG_CURRENT_API -> currentApi = stringToExistingFile(getValue(args, ++index))
 
                 ARG_MIGRATE_NULLNESS -> migrateNulls = true
 
@@ -889,8 +900,8 @@ class Options(
 
     /** Makes sure that the flag combinations make sense */
     private fun checkFlagConsistency() {
-        if (checkCompatibility && previousApi == null) {
-            throw DriverException(stderr = "$ARG_CHECK_COMPATIBILITY requires $ARG_PREVIOUS_API")
+        if (checkCompatibility && currentApi == null && previousApi == null) {
+            throw DriverException(stderr = "$ARG_CHECK_COMPATIBILITY requires $ARG_CURRENT_API")
         }
 
         if (migrateNulls && previousApi == null) {
@@ -1137,7 +1148,8 @@ class Options(
                 "source files",
 
             "$ARG_MERGE_ANNOTATIONS <file>", "An external annotations file (using IntelliJ's external " +
-                "annotations database format) to merge and overlay the sources",
+                "annotations database format) to merge and overlay the sources. A subset of .jaif files " +
+                "is also supported.",
 
             "$ARG_INPUT_API_JAR <file>", "A .jar file to read APIs from directly",
 
@@ -1161,6 +1173,7 @@ class Options(
             // TODO: Document --show-annotation!
             "$ARG_API <file>", "Generate a signature descriptor file",
             "$ARG_PRIVATE_API <file>", "Generate a signature descriptor file listing the exact private APIs",
+            "$ARG_DEX_API <file>", "Generate a DEX signature descriptor file listing the APIs",
             "$ARG_PRIVATE_DEX_API <file>", "Generate a DEX signature descriptor file listing the exact private APIs",
             "$ARG_REMOVED_API <file>", "Generate a signature descriptor file for APIs that have been removed",
             "$ARG_OUTPUT_KOTLIN_NULLS[=yes|no]", "Controls whether nullness annotations should be formatted as " +
@@ -1203,6 +1216,9 @@ class Options(
             ARG_CHECK_COMPATIBILITY, "Check compatibility with the previous API",
             ARG_CHECK_KOTLIN_INTEROP, "Check API intended to be used from both Kotlin and Java for interoperability " +
                 "issues",
+            "$ARG_CURRENT_API <signature file>", "A signature file for the current version of this " +
+                "API to check compatibility with. If not specified, $ARG_PREVIOUS_API will be used " +
+                "instead.",
             ARG_MIGRATE_NULLNESS, "Compare nullness information with the previous API and mark newly " +
                 "annotated APIs as under migration.",
             ARG_WARNINGS_AS_ERRORS, "Promote all warnings to errors",
diff --git a/src/main/java/com/android/tools/metalava/doclava1/TextCodebase.kt b/src/main/java/com/android/tools/metalava/doclava1/TextCodebase.kt
index 0e73a535f98bfb18ccb45c18544ac328dd0f98ec..2b66cf62cd5be635e168ec1403a0c78cec76f358 100644
--- a/src/main/java/com/android/tools/metalava/doclava1/TextCodebase.kt
+++ b/src/main/java/com/android/tools/metalava/doclava1/TextCodebase.kt
@@ -16,7 +16,6 @@
 
 package com.android.tools.metalava.doclava1
 
-import com.android.annotations.NonNull
 import com.android.tools.metalava.CodebaseComparator
 import com.android.tools.metalava.ComparisonVisitor
 import com.android.tools.metalava.JAVA_LANG_ANNOTATION
@@ -70,7 +69,7 @@ class TextCodebase : DefaultCodebase() {
         return mPackages.size
     }
 
-    override fun findClass(@NonNull className: String): TextClassItem? {
+    override fun findClass(className: String): TextClassItem? {
         return mAllClasses[className]
     }
 
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 68fbab0b264d873ebf50769d395ef3acf176bd60..8c31c8eef855233fc6e27c6810863cea8b3b03b3 100644
--- a/src/main/java/com/android/tools/metalava/model/ClassItem.kt
+++ b/src/main/java/com/android/tools/metalava/model/ClassItem.kt
@@ -387,6 +387,37 @@ interface ClassItem : Item {
         return null
     }
 
+    /** Finds a given method in this class matching the VM name signature */
+    fun findMethodByDesc(
+        name: String,
+        desc: String,
+        includeSuperClasses: Boolean = false,
+        includeInterfaces: Boolean = false
+    ): MethodItem? {
+        if (desc.startsWith("<init>")) {
+            constructors().asSequence()
+                .filter { it.internalDesc() == desc }
+                .forEach { return it }
+            return null
+        } else {
+            methods().asSequence()
+                .filter { it.name() == name && it.internalDesc() == desc }
+                .forEach { return it }
+        }
+
+        if (includeSuperClasses) {
+            superClass()?.findMethodByDesc(name, desc, true, includeInterfaces)?.let { return it }
+        }
+
+        if (includeInterfaces) {
+            for (itf in interfaceTypes()) {
+                val cls = itf.asClass() ?: continue
+                cls.findMethodByDesc(name, desc, includeSuperClasses, true)?.let { return it }
+            }
+        }
+        return null
+    }
+
     fun findConstructor(template: ConstructorItem): ConstructorItem? {
         constructors().asSequence()
             .filter { it.matches(template) }
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 7740930ed8f99898e5ca77e25a7372cc388a6987..03eb3878747e0a263a4b108aa8c867128c145ba9 100644
--- a/src/main/java/com/android/tools/metalava/model/MethodItem.kt
+++ b/src/main/java/com/android/tools/metalava/model/MethodItem.kt
@@ -42,7 +42,7 @@ interface MethodItem : MemberItem {
      * e.g. for the method "void create(int x, int y)" the internal name of
      * the constructor is "create" and the desc is "(II)V"
      */
-    fun internalDesc(voidConstructorTypes: Boolean): String {
+    fun internalDesc(voidConstructorTypes: Boolean = false): String {
         val sb = StringBuilder()
         sb.append("(")
 
diff --git a/src/test/java/com/android/tools/metalava/AnnotationsMergerTest.kt b/src/test/java/com/android/tools/metalava/AnnotationsMergerTest.kt
index f9472a4365d3af0a954ef773c48784602d0cb8a7..3f077c9cd33cf5dc67b54df5f9213fdca31e5095 100644
--- a/src/test/java/com/android/tools/metalava/AnnotationsMergerTest.kt
+++ b/src/test/java/com/android/tools/metalava/AnnotationsMergerTest.kt
@@ -131,4 +131,43 @@ class AnnotationsMergerTest : DriverTest() {
                 """
         )
     }
+
+    @Test
+    fun `Merge jaif files`() {
+        check(
+            sourceFiles = *arrayOf(
+                java(
+                    """
+                    package test.pkg;
+
+                    public interface Appendable {
+                        Appendable append(CharSequence csq) throws IOException;
+                    }
+                    """
+                )
+            ),
+            compatibilityMode = false,
+            outputKotlinStyleNulls = false,
+            omitCommonPackages = false,
+            mergeJaifAnnotations = """
+                //
+                // Copyright (C) 2017 The Android Open Source Project
+                //
+                package test.pkg:
+                class Appendable:
+                    method append(Ljava/lang/CharSequence;)Ltest/pkg/Appendable;:
+                        parameter #0:
+                          type: @libcore.util.Nullable
+                        // Is expected to return self
+                        return: @libcore.util.NonNull
+                """,
+            api = """
+                package test.pkg {
+                  public interface Appendable {
+                    method @androidx.annotation.NonNull public test.pkg.Appendable append(@androidx.annotation.Nullable java.lang.CharSequence);
+                  }
+                }
+                """
+        )
+    }
 }
diff --git a/src/test/java/com/android/tools/metalava/ApiFileTest.kt b/src/test/java/com/android/tools/metalava/ApiFileTest.kt
index 9643b997650c0f2b7564aad2748e47c3bae48cf3..2659f82095ea83c830ca8fe82f128f9d605ea95f 100644
--- a/src/test/java/com/android/tools/metalava/ApiFileTest.kt
+++ b/src/test/java/com/android/tools/metalava/ApiFileTest.kt
@@ -2027,7 +2027,15 @@ class ApiFileTest : DriverTest() {
                         ctor public Parent();
                       }
                     }
-                    """
+                    """,
+            dexApi = """
+                Ltest/pkg/Child;
+                Ltest/pkg/Child;-><init>()V
+                Ltest/pkg/Child;->toString()Ljava/lang/String;
+                Ltest/pkg/Parent;
+                Ltest/pkg/Parent;-><init>()V
+                Ltest/pkg/Parent;->toString()Ljava/lang/String;
+            """
         )
     }
 
diff --git a/src/test/java/com/android/tools/metalava/DriverTest.kt b/src/test/java/com/android/tools/metalava/DriverTest.kt
index da7a913041d4f04ca675e1de6845deecc51737cf..69905adcdeb53d20419142a0e45567d97a7190ba 100644
--- a/src/test/java/com/android/tools/metalava/DriverTest.kt
+++ b/src/test/java/com/android/tools/metalava/DriverTest.kt
@@ -133,6 +133,8 @@ abstract class DriverTest {
         privateApi: String? = null,
         /** The private DEX API (corresponds to --private-dex-api) */
         privateDexApi: String? = null,
+        /** The DEX API (corresponds to --dex-api) */
+        dexApi: String? = null,
         /** Expected stubs (corresponds to --stubs) */
         @Language("JAVA") stubs: Array<String> = emptyArray(),
         /** Stub source file list generated */
@@ -153,6 +155,8 @@ abstract class DriverTest {
         checkCompilation: Boolean = false,
         /** Annotations to merge in */
         @Language("XML") mergeAnnotations: String? = null,
+        /** Annotations to merge in */
+        @Language("TEXT") mergeJaifAnnotations: String? = null,
         /** An optional API signature file content to load **instead** of Java/Kotlin source files */
         @Language("TEXT") signatureSource: String? = null,
         /** An optional API jar file content to load **instead** of Java/Kotlin source files */
@@ -223,6 +227,12 @@ abstract class DriverTest {
                     "annotations output in doclava1"
             )
         }
+        if (compatibilityMode && mergeJaifAnnotations != null) {
+            fail(
+                "Can't specify both compatibilityMode and mergeJaifAnnotations: there were no " +
+                    "annotations output in doclava1"
+            )
+        }
 
         Errors.resetLevels()
 
@@ -280,6 +290,14 @@ abstract class DriverTest {
             emptyArray()
         }
 
+        val jaifAnnotationsArgs = if (mergeJaifAnnotations != null) {
+            val merged = File(project, "merged-annotations.jaif")
+            Files.asCharSink(merged, Charsets.UTF_8).write(mergeJaifAnnotations.trimIndent())
+            arrayOf("--merge-annotations", merged.path)
+        } else {
+            emptyArray()
+        }
+
         val previousApiFile = if (previousApi != null) {
             val prevApiJar = File(previousApi)
             if (prevApiJar.isFile) {
@@ -394,7 +412,7 @@ abstract class DriverTest {
 
         var apiFile: File? = null
         val apiArgs = if (api != null) {
-            apiFile = temporaryFolder.newFile("api.txt")
+            apiFile = temporaryFolder.newFile("public-api.txt")
             arrayOf("--api", apiFile.path)
         } else {
             emptyArray()
@@ -416,6 +434,14 @@ abstract class DriverTest {
             emptyArray()
         }
 
+        var dexApiFile: File? = null
+        val dexApiArgs = if (dexApi != null) {
+            dexApiFile = temporaryFolder.newFile("public-dex.txt")
+            arrayOf("--dex-api", dexApiFile.path)
+        } else {
+            emptyArray()
+        }
+
         var privateDexApiFile: File? = null
         val privateDexApiArgs = if (privateDexApi != null) {
             privateDexApiFile = temporaryFolder.newFile("private-dex.txt")
@@ -543,6 +569,7 @@ abstract class DriverTest {
             *apiArgs,
             *exactApiArgs,
             *privateApiArgs,
+            *dexApiArgs,
             *privateDexApiArgs,
             *stubsArgs,
             *stubsSourceListArgs,
@@ -553,6 +580,7 @@ abstract class DriverTest {
             *coverageStats,
             *quiet,
             *mergeAnnotationsArgs,
+            *jaifAnnotationsArgs,
             *previousApiArgs,
             *migrateNullsArguments,
             *checkCompatibilityArguments,
@@ -617,6 +645,15 @@ abstract class DriverTest {
             assertEquals(stripComments(privateApi, stripLineComments = false).trimIndent(), expectedText)
         }
 
+        if (dexApi != null && dexApiFile != null) {
+            assertTrue(
+                "${dexApiFile.path} does not exist even though --dex-api was used",
+                dexApiFile.exists()
+            )
+            val expectedText = readFile(dexApiFile, stripBlankLines, trim)
+            assertEquals(stripComments(dexApi, stripLineComments = false).trimIndent(), expectedText)
+        }
+
         if (privateDexApi != null && privateDexApiFile != null) {
             assertTrue(
                 "${privateDexApiFile.path} does not exist even though --private-dex-api was used",
@@ -892,6 +929,29 @@ abstract class DriverTest {
                 showUnannotated = showUnannotated
             )
         }
+
+        if (CHECK_OLD_DOCLAVA_TOO && checkDoclava1 && signatureSource == null &&
+            dexApi != null && dexApiFile != null
+        ) {
+            dexApiFile.delete()
+            checkSignaturesWithDoclava1(
+                api = dexApi,
+                argument = "-dexApi",
+                output = dexApiFile,
+                expected = dexApiFile,
+                sourceList = sourceList,
+                sourcePath = sourcePath,
+                packages = packages,
+                androidJar = androidJar,
+                trim = trim,
+                stripBlankLines = stripBlankLines,
+                showAnnotationArgs = showAnnotationArguments,
+                stubImportPackages = importedPackages,
+                // Workaround: -dexApi is a no-op if you don't also provide -api
+                extraArguments = arrayOf("-api", File(dexApiFile.parentFile, "dummy-api.txt").path),
+                showUnannotated = showUnannotated
+            )
+        }
     }
 
     /** Checks that the given zip annotations file contains the given XML package contents */
diff --git a/src/test/java/com/android/tools/metalava/OptionsTest.kt b/src/test/java/com/android/tools/metalava/OptionsTest.kt
index 676449b87686ec6d1dfaaa34551596fd7e7df4d6..c61501a95fe1c4e62f036392715cb336b3f6a0fa 100644
--- a/src/test/java/com/android/tools/metalava/OptionsTest.kt
+++ b/src/test/java/com/android/tools/metalava/OptionsTest.kt
@@ -53,7 +53,8 @@ API sources:
                                        when parsing the source files
 --merge-annotations <file>             An external annotations file (using IntelliJ's
                                        external annotations database format) to merge and
-                                       overlay the sources
+                                       overlay the sources. A subset of .jaif files is
+                                       also supported.
 --input-api-jar <file>                 A .jar file to read APIs from directly
 --manifest <file>                      A manifest file, used to for check permissions to
                                        cross check APIs
@@ -78,6 +79,8 @@ Extracting Signature Files:
 --api <file>                           Generate a signature descriptor file
 --private-api <file>                   Generate a signature descriptor file listing the
                                        exact private APIs
+--dex-api <file>                       Generate a DEX signature descriptor file listing
+                                       the APIs
 --private-dex-api <file>               Generate a DEX signature descriptor file listing
                                        the exact private APIs
 --removed-api <file>                   Generate a signature descriptor file for APIs that
@@ -135,6 +138,9 @@ Diffs and Checks:
 --check-compatibility                  Check compatibility with the previous API
 --check-kotlin-interop                 Check API intended to be used from both Kotlin and
                                        Java for interoperability issues
+--current-api <signature file>         A signature file for the current version of this
+                                       API to check compatibility with. If not specified,
+                                       --previous-api will be used instead.
 --migrate-nullness                     Compare nullness information with the previous API
                                        and mark newly annotated APIs as under migration.
 --warnings-as-errors                   Promote all warnings to errors