Gracefully degrade and warn when API format is incompatible

The API database format is slated to change again in an upcoming
SDK; this CL makes sure the lint database looks at the format version
and if it's an unsupported version, gracefully degrades (and creates
an API warning encouraging users to update in order for lint API
checks to be restored).

Test: Unit tests included
Bug: Not filed
Change-Id: I3c1e723f5a5f40659abfd6878cfcb30a5ef25c54
diff --git a/lint/libs/lint-api/src/main/java/com/android/tools/lint/client/api/SimplePlatformLookup.kt b/lint/libs/lint-api/src/main/java/com/android/tools/lint/client/api/SimplePlatformLookup.kt
index 3381224..27d2ebd 100644
--- a/lint/libs/lint-api/src/main/java/com/android/tools/lint/client/api/SimplePlatformLookup.kt
+++ b/lint/libs/lint-api/src/main/java/com/android/tools/lint/client/api/SimplePlatformLookup.kt
@@ -29,6 +29,8 @@
 import com.android.sdklib.BuildToolInfo
 import com.android.sdklib.IAndroidTarget
 import com.android.sdklib.OptionalLibrary
+import com.android.sdklib.repository.targets.PlatformTarget.PLATFORM_NAME
+import com.android.sdklib.repository.targets.PlatformTarget.PLATFORM_NAME_PREVIEW
 import java.io.File
 import java.io.File.separator
 import java.io.File.separatorChar
@@ -473,7 +475,14 @@
 
     override fun getVendor(): String = unsupported()
 
-    override fun getName(): String = unsupported()
+    override fun getName(): String {
+      val version = getVersion()
+      return if (version.isPreview) {
+        String.format(PLATFORM_NAME_PREVIEW, version)
+      } else {
+        String.format(PLATFORM_NAME, version)
+      }
+    }
 
     override fun getFullName(): String = unsupported()
 
diff --git a/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/ApiDetector.kt b/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/ApiDetector.kt
index 8bef58b..55b3092 100644
--- a/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/ApiDetector.kt
+++ b/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/ApiDetector.kt
@@ -61,6 +61,7 @@
 import com.android.resources.ResourceFolderType
 import com.android.resources.ResourceType
 import com.android.sdklib.SdkVersionInfo
+import com.android.tools.lint.checks.ApiLookup.UnsupportedVersionException
 import com.android.tools.lint.checks.ApiLookup.equivalentName
 import com.android.tools.lint.checks.ApiLookup.startsWithEquivalentPrefix
 import com.android.tools.lint.checks.DesugaredMethodLookup.Companion.canBeDesugaredLater
@@ -128,6 +129,7 @@
 import com.intellij.psi.PsiClass
 import com.intellij.psi.PsiClassType
 import com.intellij.psi.PsiCompiledElement
+import com.intellij.psi.PsiElement
 import com.intellij.psi.PsiEllipsisType
 import com.intellij.psi.PsiField
 import com.intellij.psi.PsiMember
@@ -136,6 +138,7 @@
 import com.intellij.psi.PsiReferenceExpression
 import com.intellij.psi.PsiSuperExpression
 import com.intellij.psi.PsiType
+import com.intellij.psi.PsiWhiteSpace
 import com.intellij.psi.util.InheritanceUtil
 import com.intellij.psi.util.PsiTreeUtil
 import com.intellij.psi.util.TypeConversionUtil
@@ -205,14 +208,20 @@
  */
 class ApiDetector : ResourceXmlDetector(), SourceCodeScanner, ResourceFolderScanner {
   private var apiDatabase: ApiLookup? = null
+  private var invalidDatabaseFormatError: String? = null
 
   override fun beforeCheckRootProject(context: Context) {
-    if (apiDatabase == null) {
-      apiDatabase = ApiLookup.get(context.client, context.project.buildTarget)
-      // We can't look up the minimum API required by the project here:
-      // The manifest file hasn't been processed yet in the -before- project hook.
-      // For now it's initialized lazily in getMinSdk(Context), but the
-      // lint infrastructure should be fixed to parse manifest file up front.
+    if (apiDatabase == null && invalidDatabaseFormatError == null) {
+      try {
+        apiDatabase = ApiLookup.get(context.client, context.project.buildTarget)
+        // We can't look up the minimum API required by the project here:
+        // The manifest file hasn't been processed yet in the -before- project hook.
+        // For now, it's initialized lazily in getMinSdk(Context), but the
+        // lint infrastructure should be fixed to parse manifest file up front.
+      } catch (e: UnsupportedVersionException) {
+        invalidDatabaseFormatError =
+          e.getDisplayMessage(context.client) + ": Lint API checks unavailable."
+      }
     }
   }
 
@@ -993,6 +1002,14 @@
 
   override fun createUastHandler(context: JavaContext): UElementHandler? {
     if (apiDatabase == null || context.isTestSource && !context.driver.checkTestSources) {
+      if (invalidDatabaseFormatError != null) {
+        // Attach the error to the first important source element (skipping comments and imports
+        // etc)
+        firstSourceElement(context.uastFile)?.let { declaration ->
+          val message = invalidDatabaseFormatError!!
+          context.report(UNSUPPORTED, declaration, context.getLocation(declaration), message)
+        }
+      }
       return null
     }
     val project = if (context.isGlobalAnalysis()) context.mainProject else context.project
@@ -1003,6 +1020,16 @@
     }
   }
 
+  private fun firstSourceElement(file: UFile?): PsiElement? {
+    file ?: return null
+    val first = file.sourcePsi.firstChild
+    var curr = first
+    while (curr is PsiWhiteSpace) {
+      curr = curr.nextSibling
+    }
+    return curr
+  }
+
   override fun getApplicableUastTypes(): List<Class<out UElement>> {
     return listOf(
       USimpleNameReferenceExpression::class.java,
diff --git a/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/ApiLookup.java b/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/ApiLookup.java
index 8630d78..471e301 100644
--- a/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/ApiLookup.java
+++ b/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/ApiLookup.java
@@ -33,9 +33,14 @@
 import com.android.tools.lint.detector.api.ExtensionSdk;
 import com.android.tools.lint.detector.api.ExtensionSdkRegistry;
 import com.android.tools.lint.detector.api.JavaContext;
+
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.hash.HashFunction;
 import com.google.common.hash.Hashing;
+
+import kotlin.text.Charsets;
+import kotlin.text.StringsKt;
+
 import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.IOException;
@@ -47,8 +52,6 @@
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
-import kotlin.text.Charsets;
-import kotlin.text.StringsKt;
 
 /**
  * Database for API checking: Allows quick lookup of a given class, method or field to see in which
@@ -76,6 +79,7 @@
     // This is really a name, not a path, but it's used externally (such as in metalava)
     // so we won't change it.
     public static final String XML_FILE_PATH = "api-versions.xml"; // relative to the SDK data/ dir
+
     /** Database moved from platform-tools to SDK in API level 26 */
     public static final int SDK_DATABASE_MIN_VERSION = 26;
 
@@ -89,6 +93,7 @@
     @VisibleForTesting static final boolean DEBUG_FORCE_REGENERATE_BINARY = false;
 
     private static final Map<AndroidVersion, SoftReference<ApiLookup>> instances = new HashMap<>();
+
     /** The API database this lookup is based on */
     @Nullable public final File xmlFile;
 
@@ -105,7 +110,10 @@
      *     will be the one originally passed in. In other words, this parameter may be ignored if
      *     the client created is not new.
      * @return a (possibly shared) instance of the API database, or null if its data can't be found
+     * @deprecated Use {@link #get(LintClient, IAndroidTarget)} instead, specifying an explicit SDK
+     *     target to use
      */
+    @Deprecated
     @Nullable
     public static ApiLookup get(@NonNull LintClient client) {
         return get(client, null);
@@ -119,10 +127,13 @@
      *     will be the one originally passed in. In other words, this parameter may be ignored if
      *     the client created is not new.
      * @param target the corresponding Android target, if known
-     * @return a (possibly shared) instance of the API database, or null if its data can't be found
+     * @return a (possibly shared) instance of the API database, or null if its data can't be found.
+     * @throws UnsupportedVersionException if the associated API database is using an incompatible
+     *     (i.e. future) format, and you need to upgrade lint to work with this version of the SDK.
      */
     @Nullable
-    public static ApiLookup get(@NonNull LintClient client, @Nullable IAndroidTarget target) {
+    public static ApiLookup get(@NonNull LintClient client, @Nullable IAndroidTarget target)
+            throws UnsupportedVersionException {
         synchronized (ApiLookup.class) {
             AndroidVersion version = target != null ? target.getVersion() : AndroidVersion.DEFAULT;
             SoftReference<ApiLookup> reference = instances.get(version);
@@ -157,7 +168,7 @@
                             // compileSdkVersion 26 and later
                             file = database;
                             version = target.getVersion();
-                            versionKey = version.getApiString();
+                            versionKey = version.getApiStringWithExtension();
                             int revision = target.getRevision();
                             if (revision != 1) {
                                 versionKey = versionKey + "rev" + revision;
@@ -226,15 +237,40 @@
                                         .toString();
                     }
 
-                    db = get(client, file, versionKey);
+                    try {
+                        db = get(client, file, versionKey);
+                    } catch (UnsupportedVersionException e) {
+                        e.target = target;
+                        // only throw the first time
+                        ApiLookup none = new ApiLookup(client, null, null);
+                        instances.put(version, new SoftReference<>(none));
+                        throw e;
+                    }
                 }
                 instances.put(version, new SoftReference<>(db));
+            } else if (db.mData == null) {
+                return null;
             }
 
             return db;
         }
     }
 
+    /**
+     * Like {@link #get(LintClient, IAndroidTarget)}, but does not throw {@link
+     * UnsupportedVersionException} for incompatible files; instead it logs a warning to the log and
+     * returns null.
+     */
+    @Nullable
+    public static ApiLookup getOrNull(@NonNull LintClient client, @Nullable IAndroidTarget target) {
+        try {
+            return get(client, target);
+        } catch (UnsupportedVersionException e) {
+            client.log(null, e.getDisplayMessage(client));
+            return null;
+        }
+    }
+
     @VisibleForTesting
     @NonNull
     static String getCacheFileName(@NonNull String xmlFileName, @NonNull String platformVersion) {
@@ -255,9 +291,12 @@
      * @param xmlFile the XML file containing configuration data to use for this database
      * @param version the version of the Android platform this API database is associated with
      * @return a (possibly shared) instance of the API database, or null if its data can't be found
+     * @throws UnsupportedVersionException if the associated API database is using an incompatible
+     *     (i.e. future) format, and you need to upgrade lint to work with this version of the SDK.
      */
     private static ApiLookup get(
-            @NonNull LintClient client, @NonNull File xmlFile, @NonNull String version) {
+            @NonNull LintClient client, @NonNull File xmlFile, @NonNull String version)
+            throws UnsupportedVersionException {
         if (!xmlFile.exists()) {
             client.log(null, "The API database file %1$s does not exist", xmlFile);
             return null;
@@ -269,7 +308,8 @@
                 throw new IllegalArgumentException(
                         String.format(
                                 Locale.US,
-                                "API database binary file specified by system property %s not found: %s",
+                                "API database binary file specified by system property %s not"
+                                        + " found: %s",
                                 API_DATABASE_BINARY_PATH_PROPERTY,
                                 binaryData));
             }
@@ -306,7 +346,7 @@
         return new ApiLookup(client, xmlFile, binaryData);
     }
 
-    private static CacheCreator cacheCreator(File xmlFile) {
+    private static CacheCreator cacheCreator(File xmlFile) throws UnsupportedVersionException {
         return (client, binaryData) -> {
             long begin = WRITE_STATS ? System.currentTimeMillis() : 0;
 
@@ -314,6 +354,10 @@
             try {
                 byte[] bytes = client.readBytes(xmlFile);
                 info = Api.parseApi(new ByteArrayInputStream(bytes));
+            } catch (UnsupportedVersionException e) {
+                // Don't pass through to the regular RuntimeException logging below
+                e.apiFile = xmlFile;
+                throw e;
             } catch (RuntimeException | IOException e) {
                 client.log(e, "Can't read API file " + xmlFile.getAbsolutePath());
                 return false;
@@ -1276,7 +1320,7 @@
     public static String getSdkExtensionField(
             @NonNull JavaContext context, int sdkId, boolean fullyQualified) {
         return getSdkExtensionField(
-                ApiLookup.get(context.getClient(), context.getProject().getBuildTarget()),
+                ApiLookup.getOrNull(context.getClient(), context.getProject().getBuildTarget()),
                 sdkId,
                 fullyQualified);
     }
@@ -1304,10 +1348,51 @@
             }
         }
 
-        boolean ok = cacheCreator(apiFile).create(client, outputFile);
-        if (ok) {
-            System.out.println("Created API database file " + outputFile);
+        try {
+            boolean ok = cacheCreator(apiFile).create(client, outputFile);
+            if (ok) {
+                client.log(null, "Created API database file " + outputFile);
+            }
+            return ok;
+        } catch (UnsupportedVersionException e) {
+            client.log(null, e.getDisplayMessage(client));
+            client.log(null, "(Database file: " + apiFile.getPath() + ")");
+            return false;
         }
-        return ok;
+    }
+
+    /**
+     * Exception thrown if the underlying database file is using a newer format than the latest one
+     * supported by the API parser and machinery.
+     */
+    public static class UnsupportedVersionException extends RuntimeException {
+        public final int requested;
+        public final int maxSupported;
+        @Nullable public File apiFile;
+        @Nullable public IAndroidTarget target;
+
+        UnsupportedVersionException(int requested, int maxSupported) {
+            super(
+                    "Unsupported API database version "
+                            + requested
+                            + "; max supported is "
+                            + maxSupported);
+            this.requested = requested;
+            this.maxSupported = maxSupported;
+        }
+
+        public String getDisplayMessage(@NonNull LintClient client) {
+            StringBuilder sb = new StringBuilder();
+            if (target != null) {
+                sb.append(target.getName());
+            } else {
+                sb.append("API database");
+            }
+            sb.append(" requires a newer version of ")
+                    .append(client.getClientDisplayName())
+                    .append(" than ")
+                    .append(client.getClientDisplayRevision());
+            return sb.toString();
+        }
     }
 }
diff --git a/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/ApiParser.java b/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/ApiParser.java
index 5ce0825..8b434d8 100644
--- a/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/ApiParser.java
+++ b/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/ApiParser.java
@@ -19,23 +19,31 @@
 import static com.android.SdkConstants.ATTR_ID;
 
 import com.android.annotations.Nullable;
+import com.android.tools.lint.checks.ApiLookup.UnsupportedVersionException;
 import com.android.tools.lint.detector.api.ExtensionSdk;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+
+import kotlin.text.StringsKt;
+
 import org.xml.sax.Attributes;
 import org.xml.sax.SAXException;
 import org.xml.sax.helpers.DefaultHandler;
 
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
 /** Parser for the simplified XML API format version 1. */
 class ApiParser extends DefaultHandler {
+    public static final int MAX_SUPPORTED_VERSION = 3;
+
     private static final String NODE_CLASS = "class";
     private static final String NODE_FIELD = "field";
     private static final String NODE_METHOD = "method";
     private static final String NODE_EXTENDS = "extends";
     private static final String NODE_IMPLEMENTS = "implements";
     private static final String NODE_SDK = "sdk";
+    private static final String NODE_API = "api";
 
     private static final String ATTR_NAME = "name";
     private static final String ATTR_SINCE = "since";
@@ -43,6 +51,7 @@
     private static final String ATTR_DEPRECATED = "deprecated";
     private static final String ATTR_REMOVED = "removed";
     private static final String ATTR_REFERENCE = "reference";
+    private static final String ATTR_VERSION = "version";
 
     // Grow class list
     private final Map<String, ApiClass> mClasses = new HashMap<>(7000);
@@ -121,6 +130,18 @@
                     reference = reference.replace('/', '.').replace('$', '.');
                 }
                 mSdks.add(new ExtensionSdk(name, Integer.decode(id), reference));
+
+            } else if (NODE_API.equals(localName)) {
+                String versionString = attributes.getValue(ATTR_VERSION);
+                if (versionString != null) {
+                    // Only enforce on the major version. Backwards compatible adjustments
+                    // can be made as long as only the minor version changes.
+                    String major = StringsKt.substringBefore(versionString, '.', versionString);
+                    int version = Integer.parseInt(major);
+                    if (version > MAX_SUPPORTED_VERSION) {
+                        throw new UnsupportedVersionException(version, MAX_SUPPORTED_VERSION);
+                    }
+                }
             }
         } finally {
             super.startElement(uri, localName, qName, attributes);
diff --git a/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/InvalidPackageDetector.java b/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/InvalidPackageDetector.java
index a28c5ba..b66230b 100644
--- a/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/InvalidPackageDetector.java
+++ b/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/InvalidPackageDetector.java
@@ -86,6 +86,7 @@
      * defined as libraries (possibly encountered later in the library traversal).
      */
     private List<Candidate> mCandidates;
+
     /**
      * Set of Java packages defined in the libraries; this means that if the user has added
      * libraries in this package namespace (such as the null annotations jars) we don't flag these.
@@ -97,7 +98,8 @@
 
     @Override
     public void beforeCheckRootProject(@NonNull Context context) {
-        mApiDatabase = ApiLookup.get(context.getClient(), context.getProject().getBuildTarget());
+        mApiDatabase =
+                ApiLookup.getOrNull(context.getClient(), context.getProject().getBuildTarget());
     }
 
     // ---- Implements ClassScanner ----
diff --git a/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/PrivateApiDetector.kt b/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/PrivateApiDetector.kt
index cfcb2ac..6398604 100644
--- a/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/PrivateApiDetector.kt
+++ b/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/PrivateApiDetector.kt
@@ -320,7 +320,7 @@
       if (aClass != null) { // Found in SDK: not internal
         return
       }
-      val apiLookup = ApiLookup.get(context.client, context.project.buildTarget) ?: return
+      val apiLookup = ApiLookup.getOrNull(context.client, context.project.buildTarget) ?: return
       isInternal = !apiLookup.containsClass(value)
     }
 
diff --git a/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/WebViewApiAvailabilityDetector.kt b/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/WebViewApiAvailabilityDetector.kt
index 9027f23..247855d 100644
--- a/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/WebViewApiAvailabilityDetector.kt
+++ b/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/WebViewApiAvailabilityDetector.kt
@@ -100,7 +100,7 @@
       }
 
       val client = context.client
-      val apiLookup = ApiLookup.get(client, context.project.buildTarget) ?: return
+      val apiLookup = ApiLookup.getOrNull(client, context.project.buildTarget) ?: return
       val api =
         apiLookup.getMethodVersions(
           WEBVIEW_CLASS_NAME,
diff --git a/lint/libs/lint-tests/src/main/java/com/android/tools/lint/checks/infrastructure/TestLintClient.java b/lint/libs/lint-tests/src/main/java/com/android/tools/lint/checks/infrastructure/TestLintClient.java
index d03657e..62de99e 100644
--- a/lint/libs/lint-tests/src/main/java/com/android/tools/lint/checks/infrastructure/TestLintClient.java
+++ b/lint/libs/lint-tests/src/main/java/com/android/tools/lint/checks/infrastructure/TestLintClient.java
@@ -1501,7 +1501,7 @@
     @Override
     public void log(Throwable exception, String format, @NonNull Object... args) {
         if (exception != null) {
-            if (task.runner.getFirstThrowable() == null) {
+            if (task != null && task.runner.getFirstThrowable() == null) {
                 task.runner.setFirstThrowable(exception);
             }
             exception.printStackTrace();
diff --git a/lint/libs/lint-tests/src/test/java/com/android/tools/lint/MainTest.java b/lint/libs/lint-tests/src/test/java/com/android/tools/lint/MainTest.java
index 23b7b40..5a561d4 100644
--- a/lint/libs/lint-tests/src/test/java/com/android/tools/lint/MainTest.java
+++ b/lint/libs/lint-tests/src/test/java/com/android/tools/lint/MainTest.java
@@ -22,23 +22,34 @@
 import static com.android.tools.lint.LintCliFlags.ERRNO_INVALID_ARGS;
 import static com.android.tools.lint.LintCliFlags.ERRNO_SUCCESS;
 import static com.android.tools.lint.checks.infrastructure.LintTestUtils.dos2unix;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import com.android.SdkConstants;
 import com.android.Version;
 import com.android.annotations.NonNull;
 import com.android.annotations.Nullable;
+import com.android.sdklib.IAndroidTarget;
 import com.android.testutils.TestUtils;
 import com.android.tools.lint.checks.AbstractCheckTest;
 import com.android.tools.lint.checks.AccessibilityDetector;
 import com.android.tools.lint.checks.DesugaredMethodLookup;
 import com.android.tools.lint.checks.infrastructure.TestFile;
+import com.android.tools.lint.checks.infrastructure.TestLintClient;
 import com.android.tools.lint.client.api.ConfigurationHierarchy;
 import com.android.tools.lint.client.api.LintDriver;
 import com.android.tools.lint.client.api.LintListener;
+import com.android.tools.lint.client.api.PlatformLookup;
 import com.android.tools.lint.detector.api.Detector;
 import com.android.tools.lint.detector.api.Issue;
 import com.android.tools.lint.detector.api.Lint;
+
+import kotlin.io.FilesKt;
+import kotlin.text.Charsets;
+import kotlin.text.StringsKt;
+
+import org.intellij.lang.annotations.Language;
+
 import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.PrintStream;
@@ -46,10 +57,6 @@
 import java.security.Permission;
 import java.util.Arrays;
 import java.util.stream.Collectors;
-import kotlin.io.FilesKt;
-import kotlin.text.Charsets;
-import kotlin.text.StringsKt;
-import org.intellij.lang.annotations.Language;
 
 @SuppressWarnings("javadoc")
 public class MainTest extends AbstractCheckTest {
@@ -1189,6 +1196,127 @@
         }
     }
 
+    public void testFutureApiVersion() throws Exception {
+        // Tests b/296372320#comment9
+        File project =
+                getProjectDir(
+                        null,
+                        manifest().minSdk(1),
+                        java(
+                                "src/test/pkg/Test.java",
+                                "package test.pkg;\n"
+                                        + "public class Test {\n"
+                                        + "    public int test(byte b) {\n"
+                                        + "        return java.lang.Byte.hashCode(b);\n"
+                                        + "    }\n"
+                                        + "}\n"),
+                        kotlin(
+                                "src/test/pkg/test2.kt",
+                                "" + "package test.pkg\n" + "fun someTest() {" + "}"),
+                        kotlin("src/blank.kt", ""),
+                        kotlin("src/test/pkg/test3.kt", "\n// My file\n"));
+
+        File root = getTempDir();
+        String codename = "future";
+        int apiLevel = 100;
+        // Stub SDK
+        File sdkHome = new File(root, "sdk");
+        File platformDir = new File(sdkHome, "platforms/" + codename);
+        File apiFile = new File(platformDir, "data/api-versions.xml");
+        File sourceProp = new File(platformDir, "source.properties");
+        TestLintClient client = createClient();
+        PlatformLookup platformLookup = client.getPlatformLookup();
+        assertNotNull(platformLookup);
+        IAndroidTarget target = platformLookup.getLatestSdkTarget(21, false, false);
+        assertNotNull(target);
+        File androidJar = target.getPath(IAndroidTarget.ANDROID_JAR).toFile();
+        FilesKt.copyTo(androidJar, new File(platformDir, androidJar.getName()), false, 1024);
+
+        //noinspection ResultOfMethodCallIgnored
+        sourceProp.getParentFile().mkdirs();
+        FilesKt.writeText(
+                sourceProp,
+                "Pkg.Desc=Android SDK Platform "
+                        + codename
+                        + "\n"
+                        + "Pkg.UserSrc=false\n"
+                        + "Platform.Version=13\n"
+                        + "AndroidVersion.CodeName="
+                        + codename
+                        + "\n"
+                        + "Pkg.Revision=2\n"
+                        + "AndroidVersion.ApiLevel="
+                        + apiLevel
+                        + "\n"
+                        + "AndroidVersion.ExtensionLevel=3\n"
+                        + "AndroidVersion.IsBaseSdk=true\n"
+                        + "Layoutlib.Api=15\n"
+                        + "Layoutlib.Revision=1\n"
+                        + "Platform.MinToolsRev=22",
+                Charsets.UTF_8);
+
+        //noinspection ResultOfMethodCallIgnored
+        apiFile.getParentFile().mkdirs();
+        FilesKt.writeText(
+                apiFile,
+                ""
+                        + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
+                        + "<api version=\"100\">\n"
+                        + "        <class name=\"java/lang/Object\" since=\"1\">\n"
+                        + "                <method name=\"&lt;init>()V\"/>\n"
+                        + "                <method name=\"clone()Ljava/lang/Object;\"/>\n"
+                        + "                <method name=\"equals(Ljava/lang/Object;)Z\"/>\n"
+                        + "                <method name=\"finalize()V\"/>\n"
+                        + "        </class>\n"
+                        // Not real API; here so we can make sure we're really using this database
+                        + "        <class name=\"android/MyTest\" since=\"14\">\n"
+                        + "        </class>\n"
+                        + "</api>\n",
+                Charsets.UTF_8);
+
+        try {
+            checkDriver(
+                    "src/test/pkg/Test.java:1: Error: Android API 100, future preview (Preview)"
+                        + " requires a newer version of Lint than $CURRENT_VERSION: Lint API checks"
+                        + " unavailable. [NewApi]\n"
+                        + "package test.pkg;\n"
+                        + "~~~~~~~~~~~~~~~~~\n"
+                        + "src/test/pkg/test2.kt:1: Error: Android API 100, future preview"
+                        + " (Preview) requires a newer version of Lint than $CURRENT_VERSION: Lint"
+                        + " API checks unavailable. [NewApi]\n"
+                        + "package test.pkg\n"
+                        + "~~~~~~~~~~~~~~~~\n"
+                        + "2 errors, 0 warnings",
+                    "",
+
+                    // Expected exit code
+                    ERRNO_SUCCESS,
+
+                    // Args
+                    new String[] {
+                        "--check",
+                        "NewApi",
+                        "--ignore",
+                        "LintError",
+                        "--sdk-home",
+                        sdkHome.getPath(),
+                        project.getPath()
+                    },
+                    new Cleanup() {
+                        @Override
+                        public String cleanup(String s) {
+                            String revision =
+                                    new LintCliClient(ToolsBaseTestLintClient.CLIENT_UNIT_TESTS)
+                                            .getClientDisplayRevision();
+                            return MainTest.this.cleanup(s).replace(revision, "$CURRENT_VERSION");
+                        }
+                    },
+                    null);
+        } finally {
+            DesugaredMethodLookup.Companion.reset();
+        }
+    }
+
     public void testFatalOnly() throws Exception {
         // This is a lint infrastructure test to make sure we correctly include issues
         // with fatal only
diff --git a/lint/libs/lint-tests/src/test/java/com/android/tools/lint/checks/ApiDetectorTest.java b/lint/libs/lint-tests/src/test/java/com/android/tools/lint/checks/ApiDetectorTest.java
index 243ff95..74ed07b 100644
--- a/lint/libs/lint-tests/src/test/java/com/android/tools/lint/checks/ApiDetectorTest.java
+++ b/lint/libs/lint-tests/src/test/java/com/android/tools/lint/checks/ApiDetectorTest.java
@@ -9592,7 +9592,7 @@
                   "Could not extract message tokens from \"" + message + "\"",
                   requiredVersion.min() >= 1
                       && requiredVersion.min() <= SdkVersionInfo.HIGHEST_KNOWN_API);
-            } else {
+            } else if (!message.contains("Lint API checks unavailable")) {
               assertNotNull("Expected a custom API quickfix if not using regular API level fix mechanism", fixData);
             }
         }
diff --git a/lint/libs/lint-tests/src/test/java/com/android/tools/lint/checks/ApiLookupTest.java b/lint/libs/lint-tests/src/test/java/com/android/tools/lint/checks/ApiLookupTest.java
index a57f6f6..9f68797 100644
--- a/lint/libs/lint-tests/src/test/java/com/android/tools/lint/checks/ApiLookupTest.java
+++ b/lint/libs/lint-tests/src/test/java/com/android/tools/lint/checks/ApiLookupTest.java
@@ -39,6 +39,7 @@
 import kotlin.text.StringsKt;
 
 import org.intellij.lang.annotations.Language;
+import org.jetbrains.annotations.NotNull;
 import org.junit.Assert;
 import org.junit.rules.TemporaryFolder;
 
@@ -649,7 +650,8 @@
         int apiLevel = 22;
         String codename = "stable";
 
-        File root = getTempDir();
+        File root = new File(getTempDir(), getName());
+        root.mkdirs();
         File outputFile = new File(root, "bin/api_database.bin");
 
         // Stub SDK
@@ -705,8 +707,8 @@
                 Charsets.UTF_8);
 
         MainTest.checkDriver(
-                "Created API database file ROOT/bin/api_database.bin",
                 "",
+                "Created API database file ROOT/bin/api_database.bin",
                 ERRNO_SUCCESS,
                 new String[] {"--XgenerateApiLookup", apiFile.getPath(), outputFile.getPath()},
                 s -> {
@@ -760,6 +762,95 @@
         }
     }
 
+    public void testFutureApiDatabaseFormat() throws IOException {
+        int apiLevel = 100;
+        String codename = "future";
+
+        File root = new File(getTempDir(), getName());
+        root.mkdirs();
+        File outputFile = new File(root, "bin/api_database.bin");
+
+        // Stub SDK
+        File sdkHome = new File(root, "sdk2");
+        File platformDir = new File(sdkHome, "platforms/" + codename);
+        File apiFile = new File(platformDir, "data/api-versions.xml");
+        File sourceProp = new File(platformDir, "source.properties");
+
+        //noinspection ResultOfMethodCallIgnored
+        sourceProp.getParentFile().mkdirs();
+        FilesKt.writeText(
+                sourceProp,
+                "Pkg.Desc=Android SDK Platform "
+                        + codename
+                        + "\n"
+                        + "Pkg.UserSrc=false\n"
+                        + "Platform.Version=13\n"
+                        + "AndroidVersion.CodeName="
+                        + codename
+                        + "\n"
+                        + "Pkg.Revision=2\n"
+                        + "AndroidVersion.ApiLevel="
+                        + apiLevel
+                        + "\n"
+                        + "AndroidVersion.ExtensionLevel=3\n"
+                        + "AndroidVersion.IsBaseSdk=true\n"
+                        + "Layoutlib.Api=15\n"
+                        + "Layoutlib.Revision=1\n"
+                        + "Platform.MinToolsRev=22",
+                Charsets.UTF_8);
+
+        //noinspection ResultOfMethodCallIgnored
+        apiFile.getParentFile().mkdirs();
+        FilesKt.writeText(
+                apiFile,
+                ""
+                        + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
+                        + "<api version=\"100\">\n"
+                        // Not real API; here so we can make sure we're really using this database
+                        + "        <class name=\"android/MyTest\" since=\"14\">\n"
+                        + "        </class>\n"
+                        + "</api>\n",
+                Charsets.UTF_8);
+
+        StringBuilder logger = new StringBuilder();
+        String canonicalRoot = root.getCanonicalPath();
+        com.android.tools.lint.checks.infrastructure.TestLintClient client =
+                new com.android.tools.lint.checks.infrastructure.TestLintClient() {
+                    @Override
+                    public File getSdkHome() {
+                        return sdkHome;
+                    }
+
+                    @Override
+                    public void log(Throwable exception, String format, @NotNull Object... args) {
+                        logger.append(
+                                String.format(format, args)
+                                        .replace(root.getPath(), "ROOT")
+                                        .replace(canonicalRoot, "ROOT"));
+                    }
+                };
+
+        List<IAndroidTarget> targets = client.getPlatformLookup().getTargets(false);
+        assertEquals(1, targets.size());
+        IAndroidTarget target = targets.get(0);
+        assertEquals(codename, target.getVersion().getCodename());
+
+        // Make sure the output isn't older than the input (the API lookup code looks for that)
+        //noinspection ResultOfMethodCallIgnored
+        outputFile.setLastModified(apiFile.lastModified());
+        try {
+            ApiLookup.get(client, target);
+            fail("Lookup for future version should have thrown an unsupported version exception");
+        } catch (ApiLookup.UnsupportedVersionException e) {
+            assertEquals(
+                    "Android API 100, future preview (Preview) requires a newer version of Lint"
+                            + " Unit Tests than unittest",
+                    e.getDisplayMessage(client));
+            // Make sure second attempt doesn't throw an exception
+            assertNull(ApiLookup.get(client, target));
+        }
+    }
+
     public void testFrom() throws IOException {
         getTempDir();
         // Note: We're *not* using mDb as lookup here (the real API database); we're using a
diff --git a/sdklib/src/main/java/com/android/sdklib/repository/targets/PlatformTarget.java b/sdklib/src/main/java/com/android/sdklib/repository/targets/PlatformTarget.java
index 1c14531..658e809 100644
--- a/sdklib/src/main/java/com/android/sdklib/repository/targets/PlatformTarget.java
+++ b/sdklib/src/main/java/com/android/sdklib/repository/targets/PlatformTarget.java
@@ -34,6 +34,7 @@
 import com.android.sdklib.repository.AndroidSdkHandler;
 import com.android.sdklib.repository.PackageParserUtils;
 import com.android.sdklib.repository.meta.DetailsTypes;
+
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
@@ -41,6 +42,7 @@
 import com.google.gson.JsonIOException;
 import com.google.gson.JsonSyntaxException;
 import com.google.gson.reflect.TypeToken;
+
 import java.io.BufferedReader;
 import java.io.File;
 import java.io.IOException;
@@ -60,15 +62,11 @@
      */
     public static final String PLATFORM_VENDOR = "Android Open Source Project";
 
-    /**
-     * "Android NN" is the default name for platform targets.
-     */
-    private static final String PLATFORM_NAME = "Android %s";
+    /** "Android NN" is the default name for platform targets. */
+    public static final String PLATFORM_NAME = "Android %s";
 
-    /**
-     * "Android NN (Preview)" is the default name for preview platform targets.
-     */
-    private static final String PLATFORM_NAME_PREVIEW = "Android %s (Preview)";
+    /** "Android NN (Preview)" is the default name for preview platform targets. */
+    public static final String PLATFORM_NAME_PREVIEW = "Android %s (Preview)";
 
     /** The {@link LocalPackage} from which this target was created. */
     private final LocalPackage mPackage;