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 {