diff --git a/src/main/java/com/android/tools/metalava/DocAnalyzer.kt b/src/main/java/com/android/tools/metalava/DocAnalyzer.kt
index 13f02a9382344b571bea27785be99c818f7265e8..cd3f4d9d531de5de9bae8f45a0c7374c2a07102b 100644
--- a/src/main/java/com/android/tools/metalava/DocAnalyzer.kt
+++ b/src/main/java/com/android/tools/metalava/DocAnalyzer.kt
@@ -15,6 +15,7 @@ import com.android.tools.metalava.model.FieldItem
 import com.android.tools.metalava.model.Item
 import com.android.tools.metalava.model.MemberItem
 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.psi.containsLinkTags
 import com.android.tools.metalava.model.visitors.ApiVisitor
@@ -33,6 +34,7 @@ import java.util.regex.Pattern
  * to convert since tags in the documentation tool used.
  */
 const val ADD_API_LEVEL_TEXT = false
+const val ADD_DEPRECATED_IN_TEXT = false
 
 /**
  * Walk over the API and apply tweaks to the documentation, such as
@@ -660,16 +662,24 @@ class DocAnalyzer(
 
         val apiLookup = ApiLookup.get(client)
 
-        codebase.accept(object : ApiVisitor(visitConstructorsAsMethods = false) {
+        val pkgApi = HashMap<PackageItem, Int?>(300)
+        codebase.accept(object : ApiVisitor(visitConstructorsAsMethods = true) {
             override fun visitMethod(method: MethodItem) {
-                val psiMethod = method.psi() as PsiMethod
+                val psiMethod = method.psi() as? PsiMethod ?: return
                 addApiLevelDocumentation(apiLookup.getMethodVersion(psiMethod), method)
                 addDeprecatedDocumentation(apiLookup.getMethodDeprecatedIn(psiMethod), method)
             }
 
             override fun visitClass(cls: ClassItem) {
                 val psiClass = cls.psi() as PsiClass
-                addApiLevelDocumentation(apiLookup.getClassVersion(psiClass), cls)
+                val since = apiLookup.getClassVersion(psiClass)
+                if (since != -1) {
+                    addApiLevelDocumentation(since, cls)
+
+                    // Compute since version for the package: it's the min of all the classes in the package
+                    val pkg = cls.containingPackage()
+                    pkgApi[pkg] = Math.min(pkgApi[pkg] ?: Integer.MAX_VALUE, since)
+                }
                 addDeprecatedDocumentation(apiLookup.getClassDeprecatedIn(psiClass), cls)
             }
 
@@ -679,30 +689,78 @@ class DocAnalyzer(
                 addDeprecatedDocumentation(apiLookup.getFieldDeprecatedIn(psiField), field)
             }
         })
+
+        val packageDocs = codebase.getPackageDocs()
+        if (packageDocs != null) {
+            for ((pkg, api) in pkgApi.entries) {
+                val code = api ?: 1
+                addApiLevelDocumentation(code, pkg)
+            }
+        }
     }
 
     private fun addApiLevelDocumentation(level: Int, item: Item) {
-        if (level > 1) {
+        if (level > 0) {
+            if (item.originallyHidden) {
+                // @SystemApi, @TestApi etc -- don't apply API levels here since we don't have accurate historical data
+                return
+            }
+            val currentCodeName = options.currentCodeName
+            val code: String = if (currentCodeName != null && level > options.currentApiLevel) {
+                currentCodeName
+            } else {
+                level.toString()
+            }
+
             @Suppress("ConstantConditionIf")
             if (ADD_API_LEVEL_TEXT) { // See 113933920: Remove "Requires API level" from method comment
-                appendDocumentation("Requires API level ${describeApiLevel(level)}", item, false)
+                val description = if (code == currentCodeName) currentCodeName else describeApiLevel(level)
+                appendDocumentation("Requires API level $description", item, false)
             }
             // Also add @since tag, unless already manually entered.
             // TODO: Override it everywhere in case the existing doc is wrong (we know
             // better), and at least for OpenJDK sources we *should* since the since tags
             // are talking about language levels rather than API levels!
-            if (!item.documentation.contains("@since")) {
-                item.appendDocumentation(level.toString(), "@since")
+            if (!item.documentation.contains("@apiSince")) {
+                item.appendDocumentation(code, "@apiSince")
+            } else {
+                reporter.report(
+                    Errors.FORBIDDEN_TAG, item, "Documentation should not specify @apiSince " +
+                        "manually; it's computed and injected at build time by $PROGRAM_NAME"
+                )
             }
         }
     }
 
     private fun addDeprecatedDocumentation(level: Int, item: Item) {
-        if (level > 1) {
-            // TODO: *pre*pend instead!
-            val description =
-                "<p class=\"caution\"><strong>This class was deprecated in API level $level.</strong></p>"
-            item.appendDocumentation(description, "@deprecated", append = false)
+        if (level > 0) {
+            if (item.originallyHidden) {
+                // @SystemApi, @TestApi etc -- don't apply API levels here since we don't have accurate historical data
+                return
+            }
+            val currentCodeName = options.currentCodeName
+            val code: String = if (currentCodeName != null && level > options.currentApiLevel) {
+                currentCodeName
+            } else {
+                level.toString()
+            }
+
+            @Suppress("ConstantConditionIf")
+            if (ADD_DEPRECATED_IN_TEXT) {
+                // TODO: *pre*pend instead!
+                val description =
+                    "<p class=\"caution\"><strong>This class was deprecated in API level $code.</strong></p>"
+                item.appendDocumentation(description, "@deprecated", append = false)
+            }
+
+            if (!item.documentation.contains("@deprecatedSince")) {
+                item.appendDocumentation(code, "@deprecatedSince")
+            } else {
+                reporter.report(
+                    Errors.FORBIDDEN_TAG, item, "Documentation should not specify @deprecatedSince " +
+                        "manually; it's computed and injected at build time by $PROGRAM_NAME"
+                )
+            }
         }
     }
 
@@ -716,12 +774,13 @@ fun ApiLookup.getClassVersion(cls: PsiClass): Int {
     return getClassVersion(owner)
 }
 
+val defaultEvaluator = DefaultJavaEvaluator(null, null)
+
 fun ApiLookup.getMethodVersion(method: PsiMethod): Int {
     val containingClass = method.containingClass ?: return -1
     val owner = containingClass.qualifiedName ?: return -1
-    val evaluator = DefaultJavaEvaluator(null, null)
-    val desc = evaluator.getMethodDescription(method, false, false)
-    return getMethodVersion(owner, method.name, desc)
+    val desc = defaultEvaluator.getMethodDescription(method, false, false)
+    return getMethodVersion(owner, if (method.isConstructor) "<init>" else method.name, desc)
 }
 
 fun ApiLookup.getFieldVersion(field: PsiField): Int {
@@ -738,8 +797,7 @@ fun ApiLookup.getClassDeprecatedIn(cls: PsiClass): Int {
 fun ApiLookup.getMethodDeprecatedIn(method: PsiMethod): Int {
     val containingClass = method.containingClass ?: return -1
     val owner = containingClass.qualifiedName ?: return -1
-    val evaluator = DefaultJavaEvaluator(null, null)
-    val desc = evaluator.getMethodDescription(method, false, false)
+    val desc = defaultEvaluator.getMethodDescription(method, false, false)
     return getMethodDeprecatedIn(owner, method.name, desc)
 }
 
diff --git a/src/main/java/com/android/tools/metalava/Driver.kt b/src/main/java/com/android/tools/metalava/Driver.kt
index 19d6749c035e37a7ab9d52ce535bf36430d7ec4b..d0d640a1616c1a43ad22e60c0aac15667e7b13ae 100644
--- a/src/main/java/com/android/tools/metalava/Driver.kt
+++ b/src/main/java/com/android/tools/metalava/Driver.kt
@@ -867,7 +867,7 @@ internal fun parseSources(
     classpath: List<File> = options.classpath,
     javaLanguageLevel: LanguageLevel = options.javaLanguageLevel,
     manifest: File? = options.manifest,
-    currentApiLevel: Int = options.currentApiLevel
+    currentApiLevel: Int = options.currentApiLevel + if (options.currentCodeName != null) 1 else 0
 ): PsiBasedCodebase {
     val projectEnvironment = createProjectEnvironment()
     val project = projectEnvironment.project
@@ -1267,7 +1267,11 @@ private fun findRoot(file: File): File? {
         if (before == '/' || before == '\\') {
             return File(path.substring(0, endIndex))
         } else {
-            reporter.report(Errors.IO_ERROR, file, "$PROGRAM_NAME was unable to determine the package name")
+            reporter.report(
+                Errors.IO_ERROR, file, "$PROGRAM_NAME was unable to determine the package name. " +
+                    "This usually means that a source file was where the directory does not seem to match the package " +
+                    "declaration; we expected the path $path to end with /${pkg.replace('.', '/') + '/' + file.name}"
+            )
         }
     }
 
diff --git a/src/main/java/com/android/tools/metalava/Options.kt b/src/main/java/com/android/tools/metalava/Options.kt
index c2c354b720c71bf50b557c2b65154f298f43ca92..07be60664f65e4f718ed68068937560f6557fdf8 100644
--- a/src/main/java/com/android/tools/metalava/Options.kt
+++ b/src/main/java/com/android/tools/metalava/Options.kt
@@ -505,6 +505,9 @@ class Options(
     /** The api level of the codebase, or -1 if not known/specified */
     var currentApiLevel = -1
 
+    /** The codename of the codebase, if it's a preview, or null if not specified */
+    var currentCodeName: String? = null
+
     /** API level XML file to generate */
     var generateApiLevelXml: File? = null
 
@@ -589,7 +592,6 @@ class Options(
         }
 
         var androidJarPatterns: MutableList<String>? = null
-        var currentCodeName: String? = null
         var currentJar: File? = null
         var updateBaselineFile: File? = null
         var baselineFile: File? = null
diff --git a/src/main/java/com/android/tools/metalava/Reporter.kt b/src/main/java/com/android/tools/metalava/Reporter.kt
index a7d0a1bbfb6416753a64cca0963286ed0fea08e3..40bb4f2d996e74bd8dbd7356b6129b782c245bde 100644
--- a/src/main/java/com/android/tools/metalava/Reporter.kt
+++ b/src/main/java/com/android/tools/metalava/Reporter.kt
@@ -308,7 +308,7 @@ open class Reporter(private val rootFolder: File? = null) {
 
         val sb = StringBuilder(100)
 
-        if (color) {
+        if (color && !isUnderTest()) {
             sb.append(terminalAttributes(bold = true))
             if (!options.omitLocations) {
                 location?.let {
diff --git a/src/main/java/com/android/tools/metalava/apilevels/AddApisFromCodebase.kt b/src/main/java/com/android/tools/metalava/apilevels/AddApisFromCodebase.kt
index 58afb9ad7c47866cc19b0bc1fbcfe198d9e9bd3e..41db85fb8762b643a42705196b2d5a450e5817a6 100644
--- a/src/main/java/com/android/tools/metalava/apilevels/AddApisFromCodebase.kt
+++ b/src/main/java/com/android/tools/metalava/apilevels/AddApisFromCodebase.kt
@@ -40,19 +40,79 @@ fun addApisFromCodebase(api: Api, apiLevel: Int, codebase: Codebase) {
             currentClass = newClass
 
             if (cls.isClass()) {
-                // Sadly it looks like the signature files use the non-public references instead
+                // The jar files historically contain package private parents instead of
+                // the real API so we need to correct the data we've already read in
+
                 val filteredSuperClass = cls.filteredSuperclass(filterReference)
                 val superClass = cls.superClass()
                 if (filteredSuperClass != superClass && filteredSuperClass != null) {
                     val existing = newClass.superClasses.firstOrNull()?.name
-                    if (existing == superClass?.internalName()) {
-                        newClass.addSuperClass(superClass?.internalName(), apiLevel)
+                    val superInternalName = superClass?.internalName()
+                    if (existing == superInternalName) {
+                        // The bytecode used to point to the old hidden super class. Point
+                        // to the real one (that the signature files referenced) instead.
+                        val removed = newClass.removeSuperClass(superInternalName)
+                        val since = removed?.since ?: apiLevel
+                        val entry = newClass.addSuperClass(filteredSuperClass.internalName(), since)
+                        // Show that it's also seen here
+                        entry.update(apiLevel)
+
+                        // Also inherit the interfaces from that API level, unless it was added later
+                        val superClassEntry = api.findClass(superInternalName)
+                        if (superClassEntry != null) {
+                            for (interfaceType in superClass!!.filteredInterfaceTypes(filterReference)) {
+                                val interfaceClass = interfaceType.asClass() ?: return
+                                var mergedSince = since
+                                val interfaceName = interfaceClass.internalName()
+                                for (itf in superClassEntry.interfaces) {
+                                    val currentInterface = itf.name
+                                    if (interfaceName == currentInterface) {
+                                        mergedSince = itf.since
+                                        break
+                                    }
+                                }
+                                newClass.addInterface(interfaceClass.internalName(), mergedSince)
+                            }
+                        }
                     } else {
                         newClass.addSuperClass(filteredSuperClass.internalName(), apiLevel)
                     }
                 } else if (superClass != null) {
                     newClass.addSuperClass(superClass.internalName(), apiLevel)
                 }
+            } else if (cls.isInterface()) {
+                val superClass = cls.superClass()
+                if (superClass != null && !superClass.isJavaLangObject()) {
+                    newClass.addInterface(superClass.internalName(), apiLevel)
+                }
+            } else if (cls.isEnum()) {
+                // Implicit super class; match convention from bytecode
+                if (newClass.name != "java/lang/Enum") {
+                    newClass.addSuperClass("java/lang/Enum", apiLevel)
+                }
+
+                // Mimic doclava enum methods
+                newClass.addMethod("valueOf(Ljava/lang/String;)L" + newClass.name + ";", apiLevel, false)
+                newClass.addMethod("values()[L" + newClass.name + ";", apiLevel, false)
+            } else if (cls.isAnnotationType()) {
+                // Implicit super class; match convention from bytecode
+                if (newClass.name != "java/lang/annotation/Annotation") {
+                    newClass.addSuperClass("java/lang/Object", apiLevel)
+                    newClass.addInterface("java/lang/annotation/Annotation", apiLevel)
+                }
+            }
+
+            // Ensure we don't end up with
+            //    -  <extends name="java/lang/Object"/>
+            //    +  <extends name="java/lang/Object" removed="29"/>
+            // which can happen because the bytecode always explicitly contains extends java.lang.Object
+            // but in the source code we don't see it, and the lack of presence of this shouldn't be
+            // taken as a sign that we no longer extend object. But only do this if the class didn't
+            // previously extend object and now extends something else.
+            if ((cls.isClass() || cls.isInterface()) &&
+                newClass.superClasses.size == 1 &&
+                newClass.superClasses[0].name == "java/lang/Object") {
+                newClass.addSuperClass("java/lang/Object", apiLevel)
             }
 
             for (interfaceType in cls.filteredInterfaceTypes(filterReference)) {
@@ -65,12 +125,11 @@ fun addApisFromCodebase(api: Api, apiLevel: Int, codebase: Codebase) {
             if (method.isPrivate || method.isPackagePrivate) {
                 return
             }
-            currentClass?.addMethod(
-                method.internalName() +
-                    // Use "V" instead of the type of the constructor for backwards compatibility
-                    // with the older bytecode
-                    method.internalDesc(voidConstructorTypes = true), apiLevel, method.deprecated
-            )
+            val name = method.internalName() +
+                // Use "V" instead of the type of the constructor for backwards compatibility
+                // with the older bytecode
+                method.internalDesc(voidConstructorTypes = true)
+            currentClass?.addMethod(name, apiLevel, method.deprecated)
         }
 
         override fun visitField(field: FieldItem) {
@@ -78,11 +137,6 @@ fun addApisFromCodebase(api: Api, apiLevel: Int, codebase: Codebase) {
                 return
             }
 
-            // We end up moving constants from interfaces in the codebase but that's not the
-            // case in older bytecode
-            if (field.isCloned()) {
-                return
-            }
             currentClass?.addField(field.internalName(), apiLevel, field.deprecated)
         }
     })
diff --git a/src/main/java/com/android/tools/metalava/apilevels/AndroidJarReader.java b/src/main/java/com/android/tools/metalava/apilevels/AndroidJarReader.java
index 59f467baac7c03bacbfe1a1f54413ed2538aa1d9..bcd8633f6c1da2ed55a0cf0b921225bd8dea82f9 100644
--- a/src/main/java/com/android/tools/metalava/apilevels/AndroidJarReader.java
+++ b/src/main/java/com/android/tools/metalava/apilevels/AndroidJarReader.java
@@ -15,6 +15,7 @@
  */
 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;
@@ -62,8 +63,14 @@ class AndroidJarReader {
     }
 
     public Api getApi() throws IOException {
-        Api api = new Api();
+        Api api;
         if (mApiLevels != null) {
+            int max = mApiLevels.length - 1;
+            if (mCodebase != null) {
+                max = mCodebase.getApiLevel();
+            }
+
+            api = new Api(max);
             for (int apiLevel = 1; apiLevel < mApiLevels.length; apiLevel++) {
                 File jar = getAndroidJarFile(apiLevel);
                 readJar(api, apiLevel, jar);
@@ -75,6 +82,7 @@ class AndroidJarReader {
                 }
             }
         } else {
+            api = new Api(mCurrentApi);
             // Get all the android.jar. They are in platforms-#
             int apiLevel = mMinApi - 1;
             while (true) {
@@ -96,6 +104,7 @@ class AndroidJarReader {
             }
         }
 
+        api.inlineFromHiddenSuperClasses();
         api.removeImplicitInterfaces();
         api.removeOverridingMethods();
 
@@ -118,17 +127,17 @@ class AndroidJarReader {
         while (entry != null) {
             String name = entry.getName();
 
-            if (name.endsWith(".class")) {
+            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*/);
 
-                // TODO: Skip package private classes; use metalava's heuristics
-
                 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);
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 f37e9c31d5fe972cc416cc4907ebef0d60a3a5f9..e2d150f9624a79cd7395cc0abd8a6f3e9692440b 100644
--- a/src/main/java/com/android/tools/metalava/apilevels/Api.java
+++ b/src/main/java/com/android/tools/metalava/apilevels/Api.java
@@ -24,11 +24,13 @@ import java.util.Map;
  */
 public class Api extends ApiElement {
     private final Map<String, ApiClass> mClasses = new HashMap<>();
+    private final int mMax;
 
-    public Api() {
+    public Api(int max) {
         // Pretend that API started from version 0 to make sure that classes existed in the first version
         // are printed with since="1".
         super("Android API");
+        mMax = max;
     }
 
     /**
@@ -61,6 +63,13 @@ public class Api extends ApiElement {
         return classElement;
     }
 
+    public ApiClass findClass(String name) {
+        if (name == null) {
+            return null;
+        }
+        return mClasses.get(name);
+    }
+
     /**
      * The bytecode visitor registers interfaces listed for a class. However,
      * a class will <b>also</b> implement interfaces implemented by the super classes.
@@ -83,4 +92,16 @@ public class Api extends ApiElement {
             classElement.removeOverridingMethods(mClasses);
         }
     }
+
+    public void inlineFromHiddenSuperClasses() {
+        Map<String, ApiClass> hidden = new HashMap<>();
+        for (ApiClass classElement : mClasses.values()) {
+            if (classElement.getHiddenUntil() < 0) { // hidden in the .jar files? (mMax==codebase, -1: jar files)
+                hidden.put(classElement.getName(), classElement);
+            }
+        }
+        for (ApiClass classElement : mClasses.values()) {
+            classElement.inlineFromHiddenSuperClasses(hidden);
+        }
+    }
 }
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 d215ec017bd95e508047190b680aa3572410efcd..7f8d0793dd612d13abbeaa4ac886448e5b90ddae 100644
--- a/src/main/java/com/android/tools/metalava/apilevels/ApiClass.java
+++ b/src/main/java/com/android/tools/metalava/apilevels/ApiClass.java
@@ -34,6 +34,13 @@ public class ApiClass extends ApiElement {
     private final List<ApiElement> mSuperClasses = new ArrayList<>();
     private final List<ApiElement> mInterfaces = new ArrayList<>();
 
+    /**
+     * If negative, never seen as public. The absolute value is the last api level it is seen as hidden in.
+     * E.g. "-5" means a class that was hidden in api levels 1-5, then it was deleted, and "8"
+     * means a class that was hidden in api levels 1-8 then made public in 9.
+     */
+    private int mPrivateUntil; // Package private class?
+
     private final Map<String, ApiElement> mFields = new HashMap<>();
     private final Map<String, ApiElement> mMethods = new HashMap<>();
 
@@ -46,11 +53,23 @@ public class ApiClass extends ApiElement {
     }
 
     public void 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);
     }
 
-    public void addSuperClass(String superClass, int since) {
-        addToArray(mSuperClasses, superClass, since);
+    public ApiElement addSuperClass(String superClass, int since) {
+        return addToArray(mSuperClasses, superClass, since);
+    }
+
+    public ApiElement removeSuperClass(String superClass) {
+        ApiElement entry = findByName(mSuperClasses, superClass);
+        if (entry != null) {
+            mSuperClasses.remove(entry);
+        }
+        return entry;
     }
 
     @NotNull
@@ -58,10 +77,34 @@ public class ApiClass extends ApiElement {
         return mSuperClasses;
     }
 
+    public void updateHidden(int api, boolean hidden) {
+        if (hidden) {
+            mPrivateUntil = -api;
+        } else {
+            mPrivateUntil = Math.abs(api);
+        }
+    }
+
+    public boolean alwaysHidden() {
+        return mPrivateUntil < 0;
+    }
+
+    public int getHiddenUntil() {
+        return mPrivateUntil;
+    }
+
+    public void setHiddenUntil(int api) {
+        mPrivateUntil = api;
+    }
+
     public void addInterface(String interfaceClass, int since) {
         addToArray(mInterfaces, interfaceClass, since);
     }
 
+    public List<ApiElement> getInterfaces() {
+        return mInterfaces;
+    }
+
     private void addToMap(Map<String, ApiElement> elements, String name, int version, boolean deprecated) {
         ApiElement element = elements.get(name);
         if (element == null) {
@@ -72,7 +115,7 @@ public class ApiClass extends ApiElement {
         }
     }
 
-    private void addToArray(Collection<ApiElement> elements, String name, int version) {
+    private ApiElement addToArray(Collection<ApiElement> elements, String name, int version) {
         ApiElement element = findByName(elements, name);
         if (element == null) {
             element = new ApiElement(name, version);
@@ -80,6 +123,7 @@ public class ApiClass extends ApiElement {
         } else {
             element.update(version);
         }
+        return element;
     }
 
     private ApiElement findByName(Collection<ApiElement> collection, String name) {
@@ -93,6 +137,9 @@ public class ApiClass extends ApiElement {
 
     @Override
     public void print(String tag, ApiElement parentElement, String indent, PrintStream stream) {
+        if (mPrivateUntil < 0) {
+            return;
+        }
         super.print(tag, false, parentElement, indent, stream);
         String innerIndent = indent + '\t';
         print(mSuperClasses, "extends", innerIndent, stream);
@@ -177,7 +224,8 @@ public class ApiClass extends ApiElement {
      * @return true if the method is an override
      */
     private boolean isOverride(ApiElement method, Map<String, ApiClass> allClasses) {
-        ApiElement localMethod = mMethods.get(method.getName());
+        String name = method.getName();
+        ApiElement localMethod = mMethods.get(name);
         if (localMethod != null && localMethod.introducedNotLaterThan(method)) {
             // This class has the method and it was introduced in at the same api level
             // as the child method, or before.
@@ -208,4 +256,35 @@ public class ApiClass extends ApiElement {
     public String toString() {
         return getName();
     }
+
+    private boolean haveInlined = false;
+
+    public void inlineFromHiddenSuperClasses(Map<String, ApiClass> hidden) {
+        if (haveInlined) {
+            return;
+        }
+        haveInlined = true;
+        for (ApiElement superClass : getSuperClasses()) {
+            ApiClass hiddenSuper = hidden.get(superClass.getName());
+            if (hiddenSuper != null) {
+                hiddenSuper.inlineFromHiddenSuperClasses(hidden);
+                Map<String, ApiElement> myMethods = this.mMethods;
+                Map<String, ApiElement> myFields = this.mFields;
+                for (Map.Entry<String, ApiElement> entry : hiddenSuper.mMethods.entrySet()) {
+                    String name = entry.getKey();
+                    ApiElement value = entry.getValue();
+                    if (!myMethods.containsKey(name)) {
+                        myMethods.put(name, value);
+                    }
+                }
+                for (Map.Entry<String, ApiElement> entry : hiddenSuper.mFields.entrySet()) {
+                    String name = entry.getKey();
+                    ApiElement value = entry.getValue();
+                    if (!myFields.containsKey(name)) {
+                        myFields.put(name, value);
+                    }
+                }
+            }
+        }
+    }
 }
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 3e06a745c2ba55a9a8dd2b3c92db5cbc602fdbdf..b9302bbd329576394dd2b9ff02810e307bedb010 100644
--- a/src/main/java/com/android/tools/metalava/apilevels/ApiElement.java
+++ b/src/main/java/com/android/tools/metalava/apilevels/ApiElement.java
@@ -68,6 +68,10 @@ public class ApiElement implements Comparable<ApiElement> {
         return mName;
     }
 
+    public int getSince() {
+        return mSince;
+    }
+
     /**
      * Checks if this API element was introduced not later than another API element.
      *
diff --git a/src/main/java/com/android/tools/metalava/doclava1/Errors.java b/src/main/java/com/android/tools/metalava/doclava1/Errors.java
index b467735481c3b4dd567fee7e049f81926abeaf28..8621c5c7c9fa0f0ec217efc7a9cf7911d7c1e023 100644
--- a/src/main/java/com/android/tools/metalava/doclava1/Errors.java
+++ b/src/main/java/com/android/tools/metalava/doclava1/Errors.java
@@ -257,6 +257,7 @@ public class Errors {
     // The plan is for this to be set as an error once (1) existing code is marked as @deprecated
     // and (2) the principle is adopted by the API council
     public static final Error EXTENDS_DEPRECATED = new Error(161, HIDDEN);
+    public static final Error FORBIDDEN_TAG = new Error(162, ERROR);
 
     // API lint
     public static final Error START_WITH_LOWER = new Error(300, ERROR, Category.API_LINT, "S1");
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 c47efbf74e0f4133963577dceca104f833a33c53..3f4ec1b10c5f644f0c38e796ab70df07b8b06374 100644
--- a/src/main/java/com/android/tools/metalava/model/Codebase.kt
+++ b/src/main/java/com/android/tools/metalava/model/Codebase.kt
@@ -64,7 +64,7 @@ interface Codebase {
 
     /**
      * The package documentation, if any - this returns overview.html files for each package
-     * that provided one. Note all codebases provide this.
+     * that provided one. Not all codebases provide this.
      */
     fun getPackageDocs(): PackageDocs?
 
diff --git a/src/main/java/com/android/tools/metalava/model/psi/CodePrinter.kt b/src/main/java/com/android/tools/metalava/model/psi/CodePrinter.kt
index bbe8f86d2e730ce13e1c21191746de1224f96802..868265a26869f47caf31e679e46dce3b235d7bc8 100644
--- a/src/main/java/com/android/tools/metalava/model/psi/CodePrinter.kt
+++ b/src/main/java/com/android/tools/metalava/model/psi/CodePrinter.kt
@@ -16,6 +16,7 @@
 
 package com.android.tools.metalava.model.psi
 
+import com.android.SdkConstants.DOT_CLASS
 import com.android.tools.lint.detector.api.ConstantEvaluator
 import com.android.tools.metalava.doclava1.Errors
 import com.android.tools.metalava.model.Codebase
@@ -89,7 +90,7 @@ open class CodePrinter(
         } else if (value is PsiLiteral) {
             return appendSourceLiteral(value.value, sb, owner)
         } else if (value is PsiClassObjectAccessExpression) {
-            sb.append(value.operand.type.canonicalText).append(".class")
+            sb.append(value.operand.type.canonicalText).append(DOT_CLASS)
             return true
         } else if (value is PsiArrayInitializerMemberValue) {
             sb.append('{')
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 a8251a51307fa4815767959359deee5c7c99d97b..c81ba1077ca96ad89afa3ca0d32d9a816f29a64e 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
@@ -121,9 +121,73 @@ abstract class PsiItem(
             return
         }
 
+        // Micro-optimization: we're very often going to be merging @apiSince and to a lesser
+        // extend @deprecatedSince into existing comments, since we're flagging every single
+        // public API. Normally merging into documentation has to be done carefully, since
+        // there could be existing versions of the tag we have to append to, and some parts
+        // of the comment needs to be present in certain places. For example, you can't
+        // just append to the description of a method by inserting something right before "*/"
+        // since you could be appending to a javadoc tag like @return.
+        //
+        // However, for @apiSince and @deprecatedSince specifically, in addition to being frequent,
+        // they will (a) never appear in existing docs, and (b) they're separate tags, which means
+        // it's safe to append them at the end. So we'll special case these two tags here, to
+        // help speed up the builds since these tags are inserted 30,000+ times for each framework
+        // 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") {
+            documentation = addUniqueTag(documentation, tagSection, comment)
+            return
+        }
+
         documentation = mergeDocumentation(documentation, element, comment.trim(), tagSection, append)
     }
 
+    private fun addUniqueTag(documentation: String, tagSection: String, commentLine: String): String {
+        assert(commentLine.indexOf('\n') == -1) // Not meant for multi-line comments
+
+        if (documentation.isBlank()) {
+            return "/** $tagSection $commentLine */"
+        }
+
+        // Already single line?
+        if (documentation.indexOf('\n') == -1) {
+            var end = documentation.lastIndexOf("*/")
+            val s = "/**\n *" + documentation.substring(3, end) + "\n * $tagSection $commentLine\n */"
+            return s
+        }
+
+        var end = documentation.lastIndexOf("*/")
+        while (end > 0 && documentation[end - 1].isWhitespace() &&
+            documentation[end - 1] != '\n') {
+            end--
+        }
+        var indent: String
+        var linePrefix = ""
+        val secondLine = documentation.indexOf('\n')
+        if (secondLine == -1) {
+            // Single line comment
+            indent = "\n * "
+        } else {
+            val indentStart = secondLine + 1
+            var indentEnd = indentStart
+            while (indentEnd < documentation.length) {
+                if (!documentation[indentEnd].isWhitespace()) {
+                    break
+                }
+                indentEnd++
+            }
+            indent = documentation.substring(indentStart, indentEnd)
+            // TODO: If it starts with "* " follow that
+            if (documentation.startsWith("* ", indentEnd)) {
+                linePrefix = "* "
+            }
+        }
+        val s = documentation.substring(0, end) + indent + linePrefix + tagSection + " " + commentLine + "\n" + indent + " */"
+        return s
+    }
+
     override fun fullyQualifiedDocumentation(): String {
         return fullyQualifiedDocumentation(documentation)
     }
diff --git a/src/test/java/com/android/tools/metalava/CompatibilityCheckTest.kt b/src/test/java/com/android/tools/metalava/CompatibilityCheckTest.kt
index 7052e281a877ac07f9e8df2376db582ae9a0bdee..da4d2d613a63d55fafc34031b4f75ba114b86edb 100644
--- a/src/test/java/com/android/tools/metalava/CompatibilityCheckTest.kt
+++ b/src/test/java/com/android/tools/metalava/CompatibilityCheckTest.kt
@@ -2656,6 +2656,69 @@ CompatibilityCheckTest : DriverTest() {
         )
     }
 
+    @Test
+    fun `Empty bundle files`() {
+        // Regression test for 124333557
+        // Makes sure we properly handle conflicting definitions of a java file in separate source roots
+        check(
+            warnings = "",
+            compatibilityMode = false,
+            checkCompatibilityApi = """
+                // Signature format: 3.0
+                package com.android.location.provider {
+                  public class LocationProviderBase1 {
+                    ctor public LocationProviderBase1();
+                    method public void onGetStatus(android.os.Bundle!);
+                  }
+                  public class LocationProviderBase2 {
+                    ctor public LocationProviderBase2();
+                    method public void onGetStatus(android.os.Bundle!);
+                  }
+                }
+                """,
+            sourceFiles = *arrayOf(
+                java(
+                    "src2/com/android/location/provider/LocationProviderBase1.java",
+                    """
+                    /** Something */
+                    package com.android.location.provider;
+                    """
+                ),
+                java(
+                    "src/com/android/location/provider/LocationProviderBase1.java",
+                    """
+                    package com.android.location.provider;
+                    import android.os.Bundle;
+
+                    public class LocationProviderBase1 {
+                        public void onGetStatus(Bundle bundle) { }
+                    }
+                    """
+                ),
+                // Try both combinations (empty java file both first on the source path
+                // and second on the source path)
+                java(
+                    "src/com/android/location/provider/LocationProviderBase2.java",
+                    """
+                    /** Something */
+                    package com.android.location.provider;
+                    """
+                ),
+                java(
+                    "src/com/android/location/provider/LocationProviderBase2.java",
+                    """
+                    package com.android.location.provider;
+                    import android.os.Bundle;
+
+                    public class LocationProviderBase2 {
+                        public void onGetStatus(Bundle bundle) { }
+                    }
+                    """
+                )
+            )
+        )
+    }
+
     // TODO: Check method signatures changing incompatibly (look especially out for adding new overloaded
     // methods and comparator getting confused!)
     //   ..equals on the method items should actually be very useful!
diff --git a/src/test/java/com/android/tools/metalava/DocAnalyzerTest.kt b/src/test/java/com/android/tools/metalava/DocAnalyzerTest.kt
index 99d76a338ad2a98b5740251f05d73cc5c30e9dd3..13a86e4e3d20a4ea767dd66e93517da3c3668a90 100644
--- a/src/test/java/com/android/tools/metalava/DocAnalyzerTest.kt
+++ b/src/test/java/com/android/tools/metalava/DocAnalyzerTest.kt
@@ -429,6 +429,7 @@ class DocAnalyzerTest : DriverTest() {
     @Test
     fun `Merge Multiple sections`() {
         check(
+            warnings = "src/android/widget/Toolbar2.java:14: error: Documentation should not specify @apiSince manually; it's computed and injected at build time by metalava [ForbiddenTag]",
             sourceFiles = *arrayOf(
                 java(
                     """
@@ -444,6 +445,14 @@ class DocAnalyzerTest : DriverTest() {
                         public int getCurrentContentInsetEnd() {
                             return 0;
                         }
+
+                        /**
+                        * @apiSince 15
+                        */
+                        @UiThread
+                        public int getCurrentContentInsetRight() {
+                            return 0;
+                        }
                     }
                     """
                 ),
@@ -468,9 +477,7 @@ class DocAnalyzerTest : DriverTest() {
             stubs = arrayOf(
                 """
                 package android.widget;
-                /**
-                 * @since 21
-                 */
+                /** @apiSince 21 */
                 @SuppressWarnings({"unchecked", "deprecation", "all"})
                 public class Toolbar2 {
                 public Toolbar2() { throw new RuntimeException("Stub!"); }
@@ -479,11 +486,19 @@ class DocAnalyzerTest : DriverTest() {
                  * <br>
                  * This method must be called on the thread that originally created
                  * this UI element. This is typically the main thread of your app.
-                 * @since 24
                  * @return blah blah blah
+                 * @apiSince 24
                  */
                 @androidx.annotation.UiThread
                 public int getCurrentContentInsetEnd() { throw new RuntimeException("Stub!"); }
+                /**
+                 * <br>
+                 * This method must be called on the thread that originally created
+                 * this UI element. This is typically the main thread of your app.
+                 * @apiSince 15
+                 */
+                @androidx.annotation.UiThread
+                public int getCurrentContentInsetRight() { throw new RuntimeException("Stub!"); }
                 }
                 """
             )
@@ -1117,16 +1132,14 @@ class DocAnalyzerTest : DriverTest() {
             stubs = arrayOf(
                 """
                 package android.widget;
-                /**
-                 * @since 21
-                 */
+                /** @apiSince 21 */
                 @SuppressWarnings({"unchecked", "deprecation", "all"})
                 public class Toolbar {
                 public Toolbar() { throw new RuntimeException("Stub!"); }
                 /**
                  * Existing documentation for {@linkplain #getCurrentContentInsetEnd()} here.
-                 * @since 24
                  * @return blah blah blah
+                 * @apiSince 24
                  */
                 public int getCurrentContentInsetEnd() { throw new RuntimeException("Stub!"); }
                 }
@@ -1175,20 +1188,19 @@ class DocAnalyzerTest : DriverTest() {
                 /**
                  * The Camera class is used to set image capture settings, start/stop preview.
                  *
-                 * @deprecated
-                 * <p class="caution"><strong>This class was deprecated in API level 21.</strong></p>
-                 *  We recommend using the new {@link android.hardware.camera2} API for new
+                 * @deprecated We recommend using the new {@link android.hardware.camera2} API for new
                  *             applications.*
+                 * @apiSince 1
+                 * @deprecatedSince 21
                  */
                 @SuppressWarnings({"unchecked", "deprecation", "all"})
                 @Deprecated
                 public class Camera {
                 public Camera() { throw new RuntimeException("Stub!"); }
                 /**
-                 * @deprecated
-                 * <p class="caution"><strong>This class was deprecated in API level 19.</strong></p>
-                 *  Use something else.
-                 * @since 14
+                 * @deprecated Use something else.
+                 * @apiSince 14
+                 * @deprecatedSince 19
                  */
                 @Deprecated public static final java.lang.String ACTION_NEW_VIDEO = "android.hardware.action.NEW_VIDEO";
                 }
@@ -1197,6 +1209,226 @@ class DocAnalyzerTest : DriverTest() {
         )
     }
 
+    @Test
+    fun `Api levels around current and preview`() {
+        check(
+            extraArguments = arrayOf(
+                ARG_CURRENT_CODENAME,
+                "Z",
+                ARG_CURRENT_VERSION,
+                "35" // not real api level of Z
+            ),
+            includeSystemApiAnnotations = true,
+            sourceFiles = *arrayOf(
+                java(
+                    """
+                    package android.pkg;
+                    import android.annotation.SystemApi;
+                    public class Test {
+                       public static final String UNIT_TEST_1 = "unit.test.1";
+                       /**
+                         * @hide
+                         */
+                        @SystemApi
+                       public static final String UNIT_TEST_2 = "unit.test.2";
+                    }
+                    """
+                ),
+                systemApiSource
+            ),
+            applyApiLevelsXml = """
+                    <?xml version="1.0" encoding="utf-8"?>
+                    <api version="2">
+                        <class name="android/pkg/Test" since="1">
+                            <field name="UNIT_TEST_1" since="35"/>
+                            <field name="UNIT_TEST_2" since="36"/>
+                        </class>
+                    </api>
+                    """,
+            checkCompilation = true,
+            checkDoclava1 = false,
+            stubs = arrayOf(
+                """
+                package android.pkg;
+                /** @apiSince 1 */
+                @SuppressWarnings({"unchecked", "deprecation", "all"})
+                public class Test {
+                public Test() { throw new RuntimeException("Stub!"); }
+                /** @apiSince 35 */
+                public static final java.lang.String UNIT_TEST_1 = "unit.test.1";
+                /**
+                 * @hide
+                 */
+                public static final java.lang.String UNIT_TEST_2 = "unit.test.2";
+                }
+                """
+            )
+        )
+    }
+
+    @Test
+    fun `No api levels on SystemApi only elements`() {
+        // @SystemApi, @TestApi etc cannot get api versions since we don't have
+        // accurate android.jar files (or even reliable api.txt/api.xml files) for them.
+        check(
+            extraArguments = arrayOf(
+                ARG_CURRENT_CODENAME,
+                "Z",
+                ARG_CURRENT_VERSION,
+                "35" // not real api level of Z
+            ),
+            sourceFiles = *arrayOf(
+                java(
+                    """
+                    package android.pkg;
+                    public class Test {
+                       public Test(int i) { }
+                       public static final String UNIT_TEST_1 = "unit.test.1";
+                       public static final String UNIT_TEST_2 = "unit.test.2";
+                    }
+                    """
+                )
+            ),
+            applyApiLevelsXml = """
+                    <?xml version="1.0" encoding="utf-8"?>
+                    <api version="2">
+                        <class name="android/pkg/Test" since="1">
+                            <method name="&lt;init>(I)V"/>
+                            <field name="UNIT_TEST_1" since="35"/>
+                            <field name="UNIT_TEST_2" since="36"/>
+                        </class>
+                    </api>
+                    """,
+            checkCompilation = true,
+            checkDoclava1 = false,
+            stubs = arrayOf(
+                """
+                package android.pkg;
+                /** @apiSince 1 */
+                @SuppressWarnings({"unchecked", "deprecation", "all"})
+                public class Test {
+                /** @apiSince 1 */
+                public Test(int i) { throw new RuntimeException("Stub!"); }
+                /** @apiSince 35 */
+                public static final java.lang.String UNIT_TEST_1 = "unit.test.1";
+                /** @apiSince Z */
+                public static final java.lang.String UNIT_TEST_2 = "unit.test.2";
+                }
+                """
+            )
+        )
+    }
+
+    @Test
+    fun `Generate API level javadocs`() {
+        // TODO: Check package-info.java conflict
+        // TODO: Test merging
+        // TODO: Test non-merging
+        check(
+            extraArguments = arrayOf(
+                ARG_CURRENT_CODENAME,
+                "Z",
+                ARG_CURRENT_VERSION,
+                "35" // not real api level of Z
+            ),
+            sourceFiles = *arrayOf(
+                java(
+                    """
+                    package android.pkg1;
+                    public class Test1 {
+                    }
+                    """
+                ),
+                java(
+                    """
+                    package android.pkg1;
+                    public class Test2 {
+                    }
+                    """
+                ),
+                source(
+                    "src/android/pkg2/package.html",
+                    """
+                    <body bgcolor="white">
+                    Some existing doc here.
+                    @deprecated
+                    <!-- comment -->
+                    </body>
+                    """
+                ).indented(),
+                java(
+                    """
+                    package android.pkg2;
+                    public class Test1 {
+                    }
+                    """
+                ),
+                java(
+                    """
+                    package android.pkg2;
+                    public class Test2 {
+                    }
+                    """
+                ),
+                java(
+                    """
+                    package android.pkg3;
+                    public class Test1 {
+                    }
+                    """
+                )
+            ),
+            applyApiLevelsXml = """
+                    <?xml version="1.0" encoding="utf-8"?>
+                    <api version="2">
+                        <class name="android/pkg1/Test1" since="15"/>
+                        <class name="android/pkg3/Test1" since="20"/>
+                    </api>
+                    """,
+            checkCompilation = true,
+            checkDoclava1 = false,
+            stubs = arrayOf(
+                """
+                package android.pkg1;
+                /** @apiSince 15 */
+                @SuppressWarnings({"unchecked", "deprecation", "all"})
+                public class Test1 {
+                public Test1() { throw new RuntimeException("Stub!"); }
+                }
+                """,
+                """
+                [android/pkg1/package-info.java]
+                /** @apiSince 15 */
+                package android.pkg1;
+                """,
+                """
+                [android/pkg2/package-info.java]
+                /**
+                 * Some existing doc here.
+                 * @deprecated
+                 * <!-- comment -->
+                 */
+                package android.pkg2;
+                """,
+                """
+                [android/pkg3/package-info.java]
+                /** @apiSince 20 */
+                package android.pkg3;
+                """
+            ),
+            stubsSourceList = """
+                TESTROOT/stubs/android/pkg1/package-info.java
+                TESTROOT/stubs/android/pkg1/Test1.java
+                TESTROOT/stubs/android/pkg1/Test2.java
+                TESTROOT/stubs/android/pkg2/package-info.java
+                TESTROOT/stubs/android/pkg2/Test1.java
+                TESTROOT/stubs/android/pkg2/Test2.java
+                TESTROOT/stubs/android/pkg3/package-info.java
+                TESTROOT/stubs/android/pkg3/Test1.java
+            """
+        )
+    }
+
     @Test
     fun `Generate overview html docs`() {
         // If a codebase provides overview.html files in the a public package,
@@ -1322,9 +1554,7 @@ class DocAnalyzerTest : DriverTest() {
             stubs = arrayOf(
                 """
                 package test.pkg;
-                /**
-                 * @since 21
-                 */
+                /** @apiSince 21 */
                 @SuppressWarnings({"unchecked", "deprecation", "all"})
                 @androidx.annotation.RequiresApi(21)
                 public class MyClass1 {
diff --git a/src/test/java/com/android/tools/metalava/DriverTest.kt b/src/test/java/com/android/tools/metalava/DriverTest.kt
index 9c27ca41e0b8a2d8992f1f2b349857ba0d723e68..8db14dc75dc4ac51776b3aa7c18cc3065f144351 100644
--- a/src/test/java/com/android/tools/metalava/DriverTest.kt
+++ b/src/test/java/com/android/tools/metalava/DriverTest.kt
@@ -420,7 +420,14 @@ abstract class DriverTest {
         if (!sourcePathDir.isDirectory) {
             sourcePathDir.mkdirs()
         }
-        val sourcePath = sourcePathDir.path
+
+        var sourcePath = sourcePathDir.path
+
+        // Make it easy to configure a source path with more than one source root: src and src2
+        if (sourceFiles.any { it.targetPath.startsWith("src2") }) {
+            sourcePath = sourcePath + File.pathSeparator + sourcePath + "2"
+        }
+
         val sourceList =
             if (signatureSource != null) {
                 sourcePathDir.mkdirs()
@@ -1245,14 +1252,26 @@ abstract class DriverTest {
         if (stubs.isNotEmpty() && stubsDir != null) {
             for (i in 0 until stubs.size) {
                 var stub = stubs[i].trimIndent()
-                val sourceFile = sourceFiles[i]
-                val targetPath = if (sourceFile.targetPath.endsWith(DOT_KT)) {
-                    // Kotlin source stubs are rewritten as .java files for now
-                    sourceFile.targetPath.substring(0, sourceFile.targetPath.length - 3) + DOT_JAVA
+
+                var targetPath: String
+                var stubFile: File
+                if (stub.startsWith("[") && stub.contains("]")) {
+                    val pathEnd = stub.indexOf("]\n")
+                    targetPath = stub.substring(1, pathEnd)
+                    stubFile = File(stubsDir, targetPath)
+                    if (stubFile.isFile) {
+                        stub = stub.substring(pathEnd + 2)
+                    }
                 } else {
-                    sourceFile.targetPath
+                    val sourceFile = sourceFiles[i]
+                    targetPath = if (sourceFile.targetPath.endsWith(DOT_KT)) {
+                        // Kotlin source stubs are rewritten as .java files for now
+                        sourceFile.targetPath.substring(0, sourceFile.targetPath.length - 3) + DOT_JAVA
+                    } else {
+                        sourceFile.targetPath
+                    }
+                    stubFile = File(stubsDir, targetPath.substring("src/".length))
                 }
-                var stubFile = File(stubsDir, targetPath.substring("src/".length))
                 if (!stubFile.isFile) {
                     if (stub.startsWith("[") && stub.contains("]")) {
                         val pathEnd = stub.indexOf("]\n")
diff --git a/src/test/java/com/android/tools/metalava/StubsTest.kt b/src/test/java/com/android/tools/metalava/StubsTest.kt
index b8719ed7a0716911fdccb69ff84ec0b1ef4454cd..f5d54cfb8fab5fa261ed9673afe5d5365c64ccd2 100644
--- a/src/test/java/com/android/tools/metalava/StubsTest.kt
+++ b/src/test/java/com/android/tools/metalava/StubsTest.kt
@@ -22,7 +22,9 @@ import com.android.tools.lint.checks.infrastructure.TestFile
 import com.android.tools.metalava.model.SUPPORT_TYPE_USE_ANNOTATIONS
 import org.intellij.lang.annotations.Language
 import org.junit.Test
+import java.io.File
 import java.io.FileNotFoundException
+import kotlin.test.assertEquals
 
 @SuppressWarnings("ALL")
 class StubsTest : DriverTest() {
@@ -4072,6 +4074,62 @@ class StubsTest : DriverTest() {
         )
     }
 
+    @Test
+    fun `Regression test for 124333557`() {
+        // Regression test for 124333557: Handle empty java files
+        check(
+            compatibilityMode = false,
+            warnings = """
+            TESTROOT/src/test/Something2.java: error: metalava was unable to determine the package name. This usually means that a source file was where the directory does not seem to match the package declaration; we expected the path TESTROOT/src/test/Something2.java to end with /test/wrong/Something2.java [IoError]
+            TESTROOT/src/test/Something2.java: error: metalava was unable to determine the package name. This usually means that a source file was where the directory does not seem to match the package declaration; we expected the path TESTROOT/src/test/Something2.java to end with /test/wrong/Something2.java [IoError]
+            """,
+            sourceFiles = *arrayOf(
+                java(
+                    "src/test/pkg/Something.java",
+                    """
+                    /** Nothing much here */
+                    """
+                ),
+                java(
+                    "src/test/pkg/Something2.java",
+                    """
+                    /** Nothing much here */
+                    package test.pkg;
+                    """
+                ),
+                java(
+                    "src/test/Something2.java",
+                    """
+                    /** Wrong package */
+                    package test.wrong;
+                    """
+                ),
+                java(
+                    """
+                    package test.pkg;
+                    public class Test {
+                        private Test() { }
+                    }
+                    """
+                )
+            ),
+            api = """
+                package test.pkg {
+                  public class Test {
+                  }
+                }
+                """,
+            projectSetup = { dir ->
+                // Make sure we handle blank/doc-only java doc files in root extraction
+                val src = listOf(File(dir, "src"))
+                val files = gatherSources(src)
+                val roots = extractRoots(files)
+                assertEquals(1, roots.size)
+                assertEquals(src[0].path, roots[0].path)
+            }
+        )
+    }
+
     // TODO: Test what happens when a class extends a hidden extends a public in separate packages,
     // and the hidden has a @hide constructor so the stub in the leaf class doesn't compile -- I should
     // check for this and fail build.
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 c62331dc2d8a7e1cefb96855b0c50ef9fec6bd02..c6681d81dfc6f015730ef5a11832572f230cb88d 100644
--- a/src/test/java/com/android/tools/metalava/apilevels/ApiGeneratorTest.kt
+++ b/src/test/java/com/android/tools/metalava/apilevels/ApiGeneratorTest.kt
@@ -17,10 +17,12 @@
 package com.android.tools.metalava.apilevels
 
 import com.android.tools.metalava.ARG_ANDROID_JAR_PATTERN
+import com.android.tools.metalava.ARG_CURRENT_CODENAME
+import com.android.tools.metalava.ARG_CURRENT_VERSION
 import com.android.tools.metalava.ARG_GENERATE_API_LEVELS
 import com.android.tools.metalava.DriverTest
 import com.android.utils.XmlUtils
-import com.google.common.truth.Truth
+import org.junit.Assert.assertFalse
 import org.junit.Assert.assertNotNull
 import org.junit.Assert.assertTrue
 import org.junit.Test
@@ -30,18 +32,23 @@ import kotlin.text.Charsets.UTF_8
 class ApiGeneratorTest : DriverTest() {
     @Test
     fun `Extract API levels`() {
-        val oldSdkJars = File("prebuilts/tools/common/api-versions")
+        var oldSdkJars = File("prebuilts/tools/common/api-versions")
         if (!oldSdkJars.isDirectory) {
-            println("Ignoring ${ApiGeneratorTest::class.java}: prebuilts not found - is \$PWD set to an Android source tree?")
-            return
+            oldSdkJars = File("../../prebuilts/tools/common/api-versions")
+            if (!oldSdkJars.isDirectory) {
+                println("Ignoring ${ApiGeneratorTest::class.java}: prebuilts not found - is \$PWD set to an Android source tree?")
+                return
+            }
         }
 
-        val platformJars = File("prebuilts/sdk")
+        var platformJars = File("prebuilts/sdk")
         if (!platformJars.isDirectory) {
-            println("Ignoring ${ApiGeneratorTest::class.java}: prebuilts not found: $platformJars")
-            return
+            platformJars = File("../../prebuilts/sdk")
+            if (!platformJars.isDirectory) {
+                println("Ignoring ${ApiGeneratorTest::class.java}: prebuilts not found: $platformJars")
+                return
+            }
         }
-
         val output = File.createTempFile("api-info", "xml")
         output.deleteOnExit()
         val outputPath = output.path
@@ -53,27 +60,33 @@ class ApiGeneratorTest : DriverTest() {
                 ARG_ANDROID_JAR_PATTERN,
                 "${oldSdkJars.path}/android-%/android.jar",
                 ARG_ANDROID_JAR_PATTERN,
-                "${platformJars.path}/%/public/android.jar"
+                "${platformJars.path}/%/public/android.jar",
+                ARG_CURRENT_CODENAME,
+                "Z",
+                ARG_CURRENT_VERSION,
+                "35" // not real api level of Z
             ),
             checkDoclava1 = false,
-            signatureSource = """
-                package test.pkg {
-                  public class MyTest {
-                    ctor public MyTest();
-                    method public int clamp(int);
-                    method public java.lang.Double convert(java.lang.Float);
-                    field public java.lang.Number myNumber;
-                  }
-                }
-                """
+            sourceFiles = *arrayOf(
+                java(
+                    """
+                    package android.pkg;
+                    public class MyTest {
+                    }
+                    """
+                )
+            )
         )
 
         assertTrue(output.isFile)
 
         val xml = output.readText(UTF_8)
-        Truth.assertThat(xml).contains("<class name=\"android/Manifest\$permission\" since=\"1\">")
-        Truth.assertThat(xml)
-            .contains("<field name=\"BIND_CARRIER_MESSAGING_SERVICE\" since=\"22\" deprecated=\"23\"/>")
+        assertTrue(xml.contains("<class name=\"android/Manifest\$permission\" since=\"1\">"))
+        assertTrue(xml.contains("<field name=\"BIND_CARRIER_MESSAGING_SERVICE\" since=\"22\" deprecated=\"23\"/>"))
+        assertTrue(xml.contains("<class name=\"android/pkg/MyTest\" since=\"36\""))
+        assertFalse(xml.contains("<implements name=\"java/lang/annotation/Annotation\" removed=\""))
+        assertFalse(xml.contains("<extends name=\"java/lang/Enum\" removed=\""))
+        assertFalse(xml.contains("<method name=\"append(C)Ljava/lang/AbstractStringBuilder;\""))
 
         val document = XmlUtils.parseDocumentSilently(xml, false)
         assertNotNull(document)