Support reading api-versions.xml version 4 files

The `since` attribute is changed to be on the form of `major.minor` with
the `minor` part always present even when it is `0`.

Change-Id: Ieb26a167a3637048aaba3e60a1631402d1b84ee1
Bug: b/356841164
diff --git a/src/main/java/com/android/tools/r8/utils/AndroidApiLevel.java b/src/main/java/com/android/tools/r8/utils/AndroidApiLevel.java
index 2f83503c..4521da0 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApiLevel.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApiLevel.java
@@ -196,4 +196,16 @@
         return MAIN;
     }
   }
+
+  public static AndroidApiLevel parseAndroidApiLevel(String apiLevel) {
+    int dotPosition = apiLevel.indexOf('.');
+    if (dotPosition == -1) {
+      return AndroidApiLevel.getAndroidApiLevel(Integer.parseInt(apiLevel));
+    } else {
+      String majorApiLevel = apiLevel.substring(0, dotPosition);
+      String minorApiLevel = apiLevel.substring(dotPosition + 1);
+      assert Integer.parseInt(minorApiLevel) >= 0;
+      return AndroidApiLevel.getAndroidApiLevel(Integer.parseInt(majorApiLevel));
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/FileUtils.java b/src/main/java/com/android/tools/r8/utils/FileUtils.java
index c3d670c..efaeb5f 100644
--- a/src/main/java/com/android/tools/r8/utils/FileUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/FileUtils.java
@@ -109,6 +109,11 @@
     return Files.readAllLines(file);
   }
 
+  public static Path writeTextFile(Path file, String text) throws IOException {
+    Files.writeString(file, text, StandardCharsets.UTF_8);
+    return file;
+  }
+
   public static Path writeTextFile(Path file, List<String> lines) throws IOException {
     Files.write(file, lines);
     return file;
diff --git a/src/test/java/com/android/tools/r8/apimodel/AndroidApiHashingDatabaseBuilderGeneratorTest.java b/src/test/java/com/android/tools/r8/apimodel/AndroidApiHashingDatabaseBuilderGeneratorTest.java
index 5d93f88..115666c 100644
--- a/src/test/java/com/android/tools/r8/apimodel/AndroidApiHashingDatabaseBuilderGeneratorTest.java
+++ b/src/test/java/com/android/tools/r8/apimodel/AndroidApiHashingDatabaseBuilderGeneratorTest.java
@@ -10,6 +10,7 @@
 import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestDiagnosticMessagesImpl;
@@ -28,16 +29,21 @@
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.IntBox;
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.ZipUtils;
 import com.google.common.collect.ImmutableList;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.List;
 import java.util.Set;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
 import org.junit.runner.RunWith;
@@ -125,6 +131,143 @@
     assertEquals(46885, numberOfMethods.get());
   }
 
+  private static String sampleVersion4ApiVersionsXml =
+      "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
+          + "<api version=\"4\">\n"
+          + "        <sdk id=\"36\" shortname=\"B-ext\" name=\"Baklava Extensions\"\n"
+          + "             reference=\"android/os/Build$VERSION_CODES$BAKLAVA\"/>\n"
+          + "\n"
+          + "        <!-- This class was introduced in Android R -->\n"
+          + "        <class name=\"android/os/ext/SdkExtensions\" since=\"30.0\">\n"
+          + "                <extends name=\"java/lang/Object\"/>\n"
+          + "                <!-- This method was introduced in Android S. It was \"backported\""
+          + " to Android R via the R extension,\n"
+          + "                     version 2. It also exists in later extensions, including the"
+          + " Baklava extension (id 36). -->\n"
+          + "                <method name=\"getAllExtensionVersions()Ljava/util/Map;\""
+          + " since=\"31.0\"\n"
+          + "                        sdks=\"30:2,31:2,33:4,34:7,35:12,36:16,0:31.0\"/>\n"
+          + "                <method name=\"getExtensionVersion(I)I\"/>\n"
+          + "                <!-- This field was introduced in Android U. It was \"backported\""
+          + " to Android R via the R extension,\n"
+          + "                     version 4. It also exists in later extensions, including the"
+          + " Baklava extension (id 36). -->\n"
+          + "                <field name=\"AD_SERVICES\" since=\"34.0\""
+          + " sdks=\"30:4,31:4,33:4,34:7,35:12,36:16,0:34.0\"/>\n"
+          + "        </class>\n"
+          + "\n"
+          + "        <!-- This class was introduced in Baklava. It does not exist in any SDK"
+          + " extension. -->\n"
+          + "        <class name=\"android/os/PlatformOnly\" since=\"36.0\">\n"
+          + "                <extends name=\"java/lang/Object\"/>\n"
+          + "                <method name=\"foo(I)V\" />\n"
+          + "        </class>\n"
+          + "</api>\n";
+
+  static class SdkExtensions {
+    int AD_SERVICES;
+  }
+
+  static class PlatformOnly {}
+
+  private static void mockAndroidJarForSampleVersion4ApiVersionsXml(Path outputPath)
+      throws Exception {
+    try (ZipOutputStream out = new ZipOutputStream(Files.newOutputStream(outputPath))) {
+      ZipUtils.writeToZipStream(
+          out,
+          "android/os/ext/SdkExtensions.class",
+          transformer(SdkExtensions.class)
+              .setClassDescriptor("Landroid/os/ext/SdkExtensions;")
+              .transform(),
+          ZipEntry.STORED);
+      ZipUtils.writeToZipStream(
+          out,
+          "android/os/PlatformOnly.class",
+          transformer(PlatformOnly.class)
+              .setClassDescriptor("Landroid/os/PlatformOnly;")
+              .transform(),
+          ZipEntry.STORED);
+    }
+  }
+
+  @Test
+  public void testApiVersionsXmlVersion4() throws Exception {
+    Path apiVersionsXml = temp.newFile("api-versions.xml").toPath();
+    FileUtils.writeTextFile(apiVersionsXml, sampleVersion4ApiVersionsXml);
+    Path apiLibrary = temp.newFile("android.jar").toPath();
+    mockAndroidJarForSampleVersion4ApiVersionsXml(apiLibrary);
+    List<ParsedApiClass> parsedApiClasses =
+        AndroidApiVersionsXmlParser.builder()
+            .setApiVersionsXml(apiVersionsXml)
+            .setAndroidJar(apiLibrary)
+            .setApiLevel(API_LEVEL)
+            .setIgnoreExemptionList(true)
+            .build()
+            .run();
+    assertEquals(2, parsedApiClasses.size());
+    assertEquals(
+        parsedApiClasses.get(0).getClassReference(),
+        Reference.classFromDescriptor("Landroid/os/ext/SdkExtensions;"));
+    assertEquals(parsedApiClasses.get(0).getApiLevel(), AndroidApiLevel.R);
+    parsedApiClasses
+        .get(0)
+        .visitMethodReferences(
+            (apiLevel, methods) -> {
+              if (apiLevel.equals(AndroidApiLevel.R)) {
+                assertEquals(
+                    methods,
+                    ImmutableList.of(
+                        Reference.methodFromDescriptor(
+                            "Landroid/os/ext/SdkExtensions;", "getExtensionVersion", "(I)I")));
+              } else if (apiLevel.equals(AndroidApiLevel.S)) {
+                assertEquals(
+                    methods,
+                    ImmutableList.of(
+                        Reference.methodFromDescriptor(
+                            "Landroid/os/ext/SdkExtensions;",
+                            "getAllExtensionVersions",
+                            "()Ljava/util/Map;")));
+              } else {
+                fail();
+              }
+            });
+    parsedApiClasses
+        .get(0)
+        .visitFieldReferences(
+            (apiLevel, fields) -> {
+              if (apiLevel.equals(AndroidApiLevel.U)) {
+                assertEquals(
+                    fields,
+                    ImmutableList.of(
+                        Reference.field(
+                            Reference.classFromDescriptor("Landroid/os/ext/SdkExtensions;"),
+                            "AD_SERVICES",
+                            Reference.typeFromDescriptor("I"))));
+              } else {
+                fail();
+              }
+            });
+    assertEquals(
+        parsedApiClasses.get(1).getClassReference(),
+        Reference.classFromDescriptor("Landroid/os/PlatformOnly;"));
+    assertEquals(parsedApiClasses.get(1).getApiLevel(), AndroidApiLevel.BAKLAVA);
+    parsedApiClasses
+        .get(1)
+        .visitMethodReferences(
+            (apiLevel, methods) -> {
+              if (apiLevel.equals(AndroidApiLevel.BAKLAVA)) {
+                assertEquals(
+                    methods,
+                    ImmutableList.of(
+                        Reference.methodFromDescriptor(
+                            "Landroid/os/PlatformOnly;", "foo", "(I)V")));
+              } else {
+                fail();
+              }
+            });
+    assertEquals(1, parsedApiClasses.get(1).getTotalMemberCount());
+  }
+
   @Test
   public void testDatabaseGenerationUpToDate() throws Exception {
     GenerateDatabaseResourceFilesResult result = generateResourcesFiles();
diff --git a/src/test/java/com/android/tools/r8/apimodel/AndroidApiVersionsXmlParser.java b/src/test/java/com/android/tools/r8/apimodel/AndroidApiVersionsXmlParser.java
index 071d6cb..5f52832 100644
--- a/src/test/java/com/android/tools/r8/apimodel/AndroidApiVersionsXmlParser.java
+++ b/src/test/java/com/android/tools/r8/apimodel/AndroidApiVersionsXmlParser.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.FieldSubject;
+import com.google.common.collect.ImmutableSet;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Comparator;
@@ -37,11 +38,13 @@
   private final Path apiVersionsXml;
   private final Path androidJar;
   private final AndroidApiLevel maxApiLevel;
+  private final boolean ignoreExemptionList;
 
   static class Builder {
     private Path apiVersionsXml;
     private Path androidJar;
     private AndroidApiLevel apiLevel;
+    boolean ignoreExemptionList = false;
 
     Builder setApiVersionsXml(Path apiVersionsXml) {
       this.apiVersionsXml = apiVersionsXml;
@@ -58,8 +61,14 @@
       return this;
     }
 
+    Builder setIgnoreExemptionList(boolean ignoreExemptionList) {
+      this.ignoreExemptionList = ignoreExemptionList;
+      return this;
+    }
+
     AndroidApiVersionsXmlParser build() {
-      return new AndroidApiVersionsXmlParser(apiVersionsXml, androidJar, apiLevel);
+      return new AndroidApiVersionsXmlParser(
+          apiVersionsXml, androidJar, apiLevel, ignoreExemptionList);
     }
   }
 
@@ -73,10 +82,14 @@
   }
 
   private AndroidApiVersionsXmlParser(
-      Path apiVersionsXml, Path androidJar, AndroidApiLevel maxApiLevel) {
+      Path apiVersionsXml,
+      Path androidJar,
+      AndroidApiLevel maxApiLevel,
+      boolean ignoreExemptionList) {
     this.apiVersionsXml = apiVersionsXml;
     this.androidJar = androidJar;
     this.maxApiLevel = maxApiLevel;
+    this.ignoreExemptionList = ignoreExemptionList;
   }
 
   private ParsedApiClass register(
@@ -87,6 +100,9 @@
   }
 
   private Set<String> getDeletedTypesMissingRemovedAttribute() {
+    if (ignoreExemptionList) {
+      return ImmutableSet.of();
+    }
     Set<String> removedTypeNames = new HashSet<>();
     if (maxApiLevel.isGreaterThanOrEqualTo(AndroidApiLevel.U)) {
       if (maxApiLevel.isLessThan(AndroidApiLevel.V)
@@ -99,15 +115,30 @@
   }
 
   private void readApiVersionsXmlFile() throws Exception {
+    boolean assertionsEnabled = false;
+    assert assertionsEnabled = true;
+    if (!assertionsEnabled) {
+      throw new Exception(
+          "Always run the api-versions.xml parser with assertions enabled. Format checks are based"
+              + " on assertions.");
+    }
     CodeInspector inspector = new CodeInspector(androidJar);
     DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
     Document document = factory.newDocumentBuilder().parse(apiVersionsXml.toFile());
+    NodeList api = document.getElementsByTagName("api");
+    assert api.getLength() == 1;
+    assert api.item(0).getNodeType() == Node.ELEMENT_NODE;
+    assert api.item(0).getAttributes().getNamedItem("version") != null;
+    String versionString = api.item(0).getAttributes().getNamedItem("version").getNodeValue();
+    int version = Integer.parseInt(versionString);
+    // Only versions 3 and 4 of api-versions.xml are supported.
+    assert version == 3 || version == 4;
     NodeList classes = document.getElementsByTagName("class");
     Set<String> exemptionList = getDeletedTypesMissingRemovedAttribute();
     for (int i = 0; i < classes.getLength(); i++) {
       Node node = classes.item(i);
       assert node.getNodeType() == Node.ELEMENT_NODE;
-      AndroidApiLevel apiLevel = getMaxAndroidApiLevelFromNode(node, AndroidApiLevel.B);
+      AndroidApiLevel apiLevel = getMaxAndroidApiLevelFromNode(node, version, AndroidApiLevel.B);
       String type = DescriptorUtils.getJavaTypeFromBinaryName(getName(node));
       ClassSubject clazz = inspector.clazz(type);
       if (!clazz.isPresent()) {
@@ -130,26 +161,31 @@
         if (isExtends(memberNode)) {
           parsedApiClass.registerSuperType(
               Reference.classFromBinaryName(getName(memberNode)),
-              hasSince(memberNode) ? getSince(memberNode) : apiLevel);
+              hasSince(memberNode) ? getSince(memberNode, version) : apiLevel);
         } else if (isImplements(memberNode)) {
           parsedApiClass.registerInterface(
               Reference.classFromBinaryName(getName(memberNode)),
-              hasSince(memberNode) ? getSince(memberNode) : apiLevel);
+              hasSince(memberNode) ? getSince(memberNode, version) : apiLevel);
         } else if (isMethod(memberNode)) {
           parsedApiClass.register(
               getMethodReference(originalReference, memberNode),
-              getMaxAndroidApiLevelFromNode(memberNode, apiLevel));
+              getMaxAndroidApiLevelFromNode(memberNode, version, apiLevel));
         } else if (isField(memberNode)) {
           // The field do not have descriptors and are supposed to be unique.
           FieldSubject fieldSubject = clazz.uniqueFieldWithOriginalName(getName(memberNode));
           if (!fieldSubject.isPresent()) {
-            assert hasRemoved(memberNode);
+            assert hasRemoved(memberNode)
+                : "Expected field "
+                    + getName(memberNode)
+                    + " in class "
+                    + type
+                    + " to be marked as removed";
             assert getRemoved(memberNode).isLessThanOrEqualTo(maxApiLevel);
             continue;
           }
           parsedApiClass.register(
               fieldSubject.getOriginalReference(),
-              getMaxAndroidApiLevelFromNode(memberNode, apiLevel));
+              getMaxAndroidApiLevelFromNode(memberNode, version, apiLevel));
         }
       }
     }
@@ -195,10 +231,11 @@
     return node.getAttributes().getNamedItem("removed") != null;
   }
 
-  private AndroidApiLevel getSince(Node node) {
+  private AndroidApiLevel getSince(Node node, int version) {
     assert hasSince(node);
     Node since = node.getAttributes().getNamedItem("since");
-    return AndroidApiLevel.getAndroidApiLevel(Integer.parseInt(since.getNodeValue()));
+    assert (version == 4) == since.getNodeValue().contains(".");
+    return AndroidApiLevel.parseAndroidApiLevel(since.getNodeValue());
   }
 
   private AndroidApiLevel getRemoved(Node node) {
@@ -207,11 +244,12 @@
     return AndroidApiLevel.getAndroidApiLevel(Integer.parseInt(removed.getNodeValue()));
   }
 
-  private AndroidApiLevel getMaxAndroidApiLevelFromNode(Node node, AndroidApiLevel defaultValue) {
+  private AndroidApiLevel getMaxAndroidApiLevelFromNode(
+      Node node, int version, AndroidApiLevel defaultValue) {
     if (node == null || !hasSince(node)) {
       return defaultValue;
     }
-    return defaultValue.max(getSince(node));
+    return defaultValue.max(getSince(node, version));
   }
 
   public static class ParsedApiClass {