Add android api database builders and initial tests for generated jar
Bug: 138781768
Bug: 190368382
Bug: 190187790
Bug: 188388130
Change-Id: I1d88285cb25b3515fcc059c7ec5ed21be4d9014a
diff --git a/build.gradle b/build.gradle
index cc51ea6..9032bc4 100644
--- a/build.gradle
+++ b/build.gradle
@@ -315,6 +315,7 @@
"android_jar/lib-v29",
"android_jar/lib-v30",
"android_jar/lib-v31",
+ "android_jar/api-versions",
"api-outlining/simple-app-dump",
"core-lambda-stubs",
"dart-sdk",
diff --git a/src/main/java/com/android/tools/r8/androidapi/AndroidApiClass.java b/src/main/java/com/android/tools/r8/androidapi/AndroidApiClass.java
new file mode 100644
index 0000000..4a9c41a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/androidapi/AndroidApiClass.java
@@ -0,0 +1,65 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.androidapi;
+
+import com.android.tools.r8.KeepForSubclassing;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.FieldReference;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.references.TypeReference;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.TraversalContinuation;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.BiFunction;
+
+/** This is a base class for all generated classes from api-versions.xml. */
+@KeepForSubclassing
+public abstract class AndroidApiClass {
+
+ private final ClassReference classReference;
+
+ public AndroidApiClass(ClassReference classReference) {
+ this.classReference = classReference;
+ }
+
+ public abstract AndroidApiLevel getApiLevel();
+
+ public abstract TraversalContinuation visitFields(
+ BiFunction<FieldReference, AndroidApiLevel, TraversalContinuation> visitor);
+
+ public abstract TraversalContinuation visitMethods(
+ BiFunction<MethodReference, AndroidApiLevel, TraversalContinuation> visitor);
+
+ protected TraversalContinuation visitField(
+ String name,
+ String typeDescriptor,
+ int apiLevel,
+ BiFunction<FieldReference, AndroidApiLevel, TraversalContinuation> visitor) {
+ return visitor.apply(
+ Reference.field(classReference, name, Reference.typeFromDescriptor(typeDescriptor)),
+ AndroidApiLevel.getAndroidApiLevel(apiLevel));
+ }
+
+ protected TraversalContinuation visitMethod(
+ String name,
+ String[] formalTypeDescriptors,
+ String returnType,
+ int apiLevel,
+ BiFunction<MethodReference, AndroidApiLevel, TraversalContinuation> visitor) {
+ List<TypeReference> typeReferenceList = new ArrayList<>(formalTypeDescriptors.length);
+ for (String formalTypeDescriptor : formalTypeDescriptors) {
+ typeReferenceList.add(Reference.typeFromDescriptor(formalTypeDescriptor));
+ }
+ return visitor.apply(
+ Reference.method(
+ classReference,
+ name,
+ typeReferenceList,
+ returnType == null ? null : Reference.returnTypeFromDescriptor(returnType)),
+ AndroidApiLevel.getAndroidApiLevel(apiLevel));
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/references/Reference.java b/src/main/java/com/android/tools/r8/references/Reference.java
index 795c3bb..e6a829b 100644
--- a/src/main/java/com/android/tools/r8/references/Reference.java
+++ b/src/main/java/com/android/tools/r8/references/Reference.java
@@ -161,6 +161,22 @@
returnTypeDescriptor.equals("V") ? null : typeFromDescriptor(returnTypeDescriptor));
}
+ /** Get a method reference from class reference, method name and signature. */
+ public static MethodReference methodFromDescriptor(
+ ClassReference classReference, String methodName, String methodDescriptor) {
+ ImmutableList.Builder<TypeReference> builder = ImmutableList.builder();
+ for (String parameterTypeDescriptor :
+ DescriptorUtils.getArgumentTypeDescriptors(methodDescriptor)) {
+ builder.add(typeFromDescriptor(parameterTypeDescriptor));
+ }
+ String returnTypeDescriptor = DescriptorUtils.getReturnTypeDescriptor(methodDescriptor);
+ return method(
+ classReference,
+ methodName,
+ builder.build(),
+ returnTypeDescriptor.equals("V") ? null : typeFromDescriptor(returnTypeDescriptor));
+ }
+
public static MethodReference classConstructor(ClassReference type) {
return method(type, "<clinit>", Collections.emptyList(), null);
}
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 6ea2f40..a85a657 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApiLevel.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApiLevel.java
@@ -3,12 +3,14 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.utils;
+import com.android.tools.r8.Keep;
import com.android.tools.r8.errors.Unreachable;
import com.android.tools.r8.utils.structural.Ordered;
import java.util.Arrays;
import java.util.List;
/** Android API level description */
+@Keep
public enum AndroidApiLevel implements Ordered<AndroidApiLevel> {
B(1),
B_1_1(2),
diff --git a/src/main/java/com/android/tools/r8/utils/ArrayUtils.java b/src/main/java/com/android/tools/r8/utils/ArrayUtils.java
index 2616514..9a863be 100644
--- a/src/main/java/com/android/tools/r8/utils/ArrayUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ArrayUtils.java
@@ -171,4 +171,8 @@
newArray[ts.length] = element;
return newArray;
}
+
+ public static <T> T first(T[] ts) {
+ return ts[0];
+ }
}
diff --git a/src/main/java/com/android/tools/r8/utils/TraversalContinuation.java b/src/main/java/com/android/tools/r8/utils/TraversalContinuation.java
index 294a210..e3b1753 100644
--- a/src/main/java/com/android/tools/r8/utils/TraversalContinuation.java
+++ b/src/main/java/com/android/tools/r8/utils/TraversalContinuation.java
@@ -3,7 +3,11 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.utils;
+import com.android.tools.r8.Keep;
+
/** Two value continuation value to indicate the continuation of a loop/traversal. */
+/* This class is used for building up api class member traversals. */
+@Keep
public enum TraversalContinuation {
CONTINUE,
BREAK;
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index 19c77fb..e51cf99 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -1708,6 +1708,21 @@
return DescriptorUtils.javaTypeToDescriptor(typeName(clazz));
}
+ public static String methodDescriptor(Method method) {
+ return Reference.methodFromMethod(method).getMethodDescriptor();
+ }
+
+ public static String methodDescriptor(Class<?> returnType, Class<?>... parameters) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("(");
+ for (Class<?> parameter : parameters) {
+ sb.append(descriptor(parameter));
+ }
+ sb.append(")");
+ sb.append(returnType == null ? "V" : descriptor(returnType));
+ return sb.toString();
+ }
+
public static PathOrigin getOrigin(Class<?> clazz) {
return new PathOrigin(ToolHelper.getClassFileForTestClass(clazz));
}
diff --git a/src/test/java/com/android/tools/r8/TestParametersBuilder.java b/src/test/java/com/android/tools/r8/TestParametersBuilder.java
index e3570be..fddd0d4 100644
--- a/src/test/java/com/android/tools/r8/TestParametersBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestParametersBuilder.java
@@ -62,6 +62,10 @@
return withCfRuntimeFilter(vm -> vm == runtime);
}
+ public TestParametersBuilder withSystemRuntime() {
+ return withCfRuntimeFilter(TestParametersBuilder::isSystemJdk);
+ }
+
/** Add all available CF runtimes. */
public TestParametersBuilder withCfRuntimes() {
return withCfRuntimeFilter(vm -> true);
diff --git a/src/test/java/com/android/tools/r8/TestRuntime.java b/src/test/java/com/android/tools/r8/TestRuntime.java
index 8b99ca8..a51fcf0 100644
--- a/src/test/java/com/android/tools/r8/TestRuntime.java
+++ b/src/test/java/com/android/tools/r8/TestRuntime.java
@@ -159,7 +159,7 @@
.build();
}
- public static TestRuntime getSystemRuntime() {
+ public static CfRuntime getSystemRuntime() {
String version = System.getProperty("java.version");
String home = System.getProperty("java.home");
if (version == null || version.isEmpty() || home == null || home.isEmpty()) {
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index 50f5346..f3108e5 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -165,6 +165,8 @@
public static final Path D8_JAR = Paths.get(LIBS_DIR, "d8.jar");
public static final Path R8_JAR = Paths.get(LIBS_DIR, "r8.jar");
public static final Path R8_WITH_DEPS_JAR = Paths.get(LIBS_DIR, "r8_with_deps.jar");
+ public static final Path R8_WITHOUT_DEPS_JAR =
+ Paths.get(LIBS_DIR, "r8_no_manifest_without_deps.jar");
public static final Path R8_WITH_RELOCATED_DEPS_JAR =
Paths.get(LIBS_DIR, "r8_with_relocated_deps.jar");
public static final Path R8_WITH_DEPS_11_JAR = Paths.get(LIBS_DIR, "r8_with_deps_11.jar");
diff --git a/src/test/java/com/android/tools/r8/apimodel/AndroidApiDatabaseBuilderGenerator.java b/src/test/java/com/android/tools/r8/apimodel/AndroidApiDatabaseBuilderGenerator.java
new file mode 100644
index 0000000..5e75e09
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodel/AndroidApiDatabaseBuilderGenerator.java
@@ -0,0 +1,468 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.apimodel;
+
+import static com.android.tools.r8.utils.FunctionUtils.ignoreArgument;
+import static org.objectweb.asm.Opcodes.AASTORE;
+import static org.objectweb.asm.Opcodes.ACONST_NULL;
+import static org.objectweb.asm.Opcodes.ALOAD;
+import static org.objectweb.asm.Opcodes.ANEWARRAY;
+import static org.objectweb.asm.Opcodes.ARETURN;
+import static org.objectweb.asm.Opcodes.DUP;
+import static org.objectweb.asm.Opcodes.F_SAME1;
+import static org.objectweb.asm.Opcodes.IFEQ;
+import static org.objectweb.asm.Opcodes.INVOKEINTERFACE;
+import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
+import static org.objectweb.asm.Opcodes.INVOKESTATIC;
+import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
+import static org.objectweb.asm.Opcodes.NEW;
+import static org.objectweb.asm.Opcodes.POP;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.androidapi.AndroidApiClass;
+import com.android.tools.r8.apimodel.AndroidApiVersionsXmlParser.ParsedApiClass;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.TypeReference;
+import com.android.tools.r8.transformers.ClassFileTransformer.MethodPredicate;
+import com.android.tools.r8.transformers.MethodTransformer;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.TraversalContinuation;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.Opcodes;
+
+public class AndroidApiDatabaseBuilderGenerator extends TestBase {
+
+ public static String generatedMainDescriptor() {
+ return descriptor(AndroidApiDatabaseBuilderTemplate.class).replace("Template", "");
+ }
+
+ /**
+ * Generate the classes needed for looking up api level of references in the android.jar.
+ *
+ * <p>For each api class we generate a from AndroidApiDatabaseClassTemplate, extending
+ * AndroidApiClass, such that all members can be traversed. Looking up the class reference
+ * directly in one method will generate to much code and would probably be inefficient so we first
+ * do a case on the package and then do a check on the simple name.
+ *
+ * <p>We therefore create a class file for each package, based on
+ * AndroidApiDatabasePackageTemplate, that will do the dispatch to create a new
+ * AndroidApiDatabaseClass for class in the package.
+ *
+ * <p>We then have a single entry-point, AndroidApiDatabaseBuilder, based on
+ * AndroidApiDatabaseBuilderTemplate, that will do the dispatch to AndroidApiDatabasePackage based
+ * on the package name.
+ */
+ public static void generate(List<ParsedApiClass> apiClasses, BiConsumer<String, byte[]> consumer)
+ throws Exception {
+ Map<String, List<ParsedApiClass>> packageToClassesMap = new HashMap<>();
+ List<String> packages =
+ apiClasses.stream()
+ .map(
+ apiClass -> {
+ String packageName =
+ DescriptorUtils.getPackageNameFromDescriptor(
+ apiClass.getClassReference().getDescriptor());
+ packageToClassesMap
+ .computeIfAbsent(packageName, ignoreArgument(ArrayList::new))
+ .add(apiClass);
+ return packageName;
+ })
+ .sorted()
+ .distinct()
+ .collect(Collectors.toList());
+
+ for (ParsedApiClass apiClass : apiClasses) {
+ consumer.accept(
+ getApiClassDescriptor(apiClass),
+ transformer(AndroidApiDatabaseClassTemplate.class)
+ .setClassDescriptor(getApiClassDescriptor(apiClass))
+ .addMethodTransformer(getInitTransformer(apiClass))
+ .addMethodTransformer(getApiLevelTransformer(apiClass))
+ .addMethodTransformer(getVisitFieldsTransformer(apiClass))
+ .addMethodTransformer(getVisitMethodsTransformer(apiClass))
+ .removeMethods(MethodPredicate.onName("placeHolder"))
+ .removeMethods(MethodPredicate.onName("placeHolderForGetApiLevel"))
+ .removeMethods(MethodPredicate.onName("placeHolderForInit"))
+ .transform());
+ }
+
+ for (String pkg : packages) {
+ consumer.accept(
+ getPackageBuilderDescriptor(pkg),
+ transformer(AndroidApiDatabasePackageTemplate.class)
+ .setClassDescriptor(getPackageBuilderDescriptor(pkg))
+ .addMethodTransformer(getBuildClassTransformer(packageToClassesMap.get(pkg)))
+ .removeMethods(MethodPredicate.onName("placeHolder"))
+ .transform());
+ }
+
+ consumer.accept(
+ generatedMainDescriptor(),
+ transformer(AndroidApiDatabaseBuilderTemplate.class)
+ .setClassDescriptor(generatedMainDescriptor())
+ .addMethodTransformer(getVisitApiClassesTransformer(apiClasses))
+ .addMethodTransformer(getBuildPackageTransformer(packages))
+ .removeMethods(MethodPredicate.onName("placeHolder"))
+ .transform());
+ }
+
+ private static String getPackageBuilderDescriptor(String pkg) {
+ return DescriptorUtils.javaTypeToDescriptor(
+ AndroidApiDatabasePackageTemplate.class
+ .getTypeName()
+ .replace("Template", "ForPackage_" + pkg.replace(".", "_")));
+ }
+
+ private static String getApiClassDescriptor(ParsedApiClass apiClass) {
+ return DescriptorUtils.javaTypeToDescriptor(
+ AndroidApiDatabaseClassTemplate.class
+ .getTypeName()
+ .replace(
+ "Template",
+ "ForClass_" + apiClass.getClassReference().getTypeName().replace(".", "_")));
+ }
+
+ // The transformer below changes AndroidApiDatabaseClassTemplate.<init> from:
+ // super(Reference.classFromDescriptor(placeHolderForInit()));
+ // into
+ // super(Reference.classFromDescriptor("<class-descriptor>"));
+ private static MethodTransformer getInitTransformer(ParsedApiClass apiClass) {
+ return replaceCode(
+ "placeHolderForInit",
+ transformer -> {
+ transformer.visitLdcInsn(apiClass.getClassReference().getDescriptor());
+ });
+ }
+
+ // The transformer below changes AndroidApiDatabaseClassTemplate.getApiLevel from:
+ // return placeHolderForGetApiLevel();
+ // into
+ // return AndroidApiLevel.getAndroidApiLevel(<apiLevel>);
+ private static MethodTransformer getApiLevelTransformer(ParsedApiClass apiClass)
+ throws NoSuchMethodException {
+ Method getAndroidApiLevel = AndroidApiLevel.class.getMethod("getAndroidApiLevel", int.class);
+ return replaceCode(
+ "placeHolderForGetApiLevel",
+ transformer -> {
+ transformer.visitLdcInsn(apiClass.getApiLevel().getLevel());
+ transformer.visitMethodInsn(
+ INVOKESTATIC,
+ binaryName(AndroidApiLevel.class),
+ getAndroidApiLevel.getName(),
+ methodDescriptor(getAndroidApiLevel),
+ false);
+ });
+ }
+
+ // The transformer below changes AndroidApiDatabaseClassTemplate.visitFields from:
+ // placeHolder();
+ // return TraversalContinuation.CONTINUE;
+ // into
+ // TraversalContinuation s1 = visitField("field1", "descriptor1", apiLevel1, visitor)
+ // if (s1.shouldBreak()) {
+ // return s1;
+ // }
+ // TraversalContinuation s2 = visitField("field2", "descriptor2", apiLevel2, visitor)
+ // if (s2.shouldBreak()) {
+ // return s2;
+ // }
+ // ...
+ // return TraversalContinuation.CONTINUE;
+ private static MethodTransformer getVisitFieldsTransformer(ParsedApiClass apiClass)
+ throws NoSuchMethodException {
+ Method shouldBreak = TraversalContinuation.class.getMethod("shouldBreak");
+ return replaceCode(
+ "placeHolderForVisitFields",
+ transformer -> {
+ apiClass.visitFieldReferences(
+ (apiLevel, references) -> {
+ references.forEach(
+ reference -> {
+ transformer.visitVarInsn(ALOAD, 0);
+ transformer.visitLdcInsn(reference.getFieldName());
+ transformer.visitLdcInsn(reference.getFieldType().getDescriptor());
+ transformer.visitLdcInsn(apiLevel.getLevel());
+ transformer.visitVarInsn(ALOAD, 1);
+ transformer.visitMethodInsn(
+ INVOKEVIRTUAL,
+ binaryName(AndroidApiClass.class),
+ "visitField",
+ methodDescriptor(
+ TraversalContinuation.class,
+ String.class,
+ String.class,
+ int.class,
+ BiFunction.class),
+ false);
+ // Note that instead of storing the result here, we dup it on the stack.
+ transformer.visitInsn(DUP);
+ transformer.visitMethodInsn(
+ INVOKEVIRTUAL,
+ binaryName(TraversalContinuation.class),
+ shouldBreak.getName(),
+ methodDescriptor(shouldBreak),
+ false);
+ Label label = new Label();
+ transformer.visitJumpInsn(IFEQ, label);
+ transformer.visitInsn(ARETURN);
+ transformer.visitLabel(label);
+ transformer.visitFrame(
+ F_SAME1,
+ 0,
+ new Object[] {},
+ 1,
+ new Object[] {binaryName(TraversalContinuation.class)});
+ // The pop here is needed to remove the dupped value in the case we do not
+ // return.
+ transformer.visitInsn(POP);
+ });
+ });
+ });
+ }
+
+ // The transformer below changes AndroidApiDatabaseClassTemplate.visitMethods from:
+ // placeHolderForVisitMethods();
+ // return TraversalContinuation.CONTINUE;
+ // into
+ // TraversalContinuation s1 = visitMethod(
+ // "method1", new String[] { "param11", ... , "param1X" }, null/return1, apiLevel1, visitor)
+ // if (s1.shouldBreak()) {
+ // return s1;
+ // }
+ // TraversalContinuation s1 = visitMethod(
+ // "method2", new String[] { "param21", ... , "param2X" }, null/return2, apiLevel2, visitor)
+ // if (s2.shouldBreak()) {
+ // return s2;
+ // }
+ // ...
+ // return TraversalContinuation.CONTINUE;
+ private static MethodTransformer getVisitMethodsTransformer(ParsedApiClass apiClass)
+ throws NoSuchMethodException {
+ Method shouldBreak = TraversalContinuation.class.getMethod("shouldBreak");
+ return replaceCode(
+ "placeHolderForVisitMethods",
+ transformer -> {
+ apiClass.visitMethodReferences(
+ (apiLevel, references) -> {
+ references.forEach(
+ reference -> {
+ transformer.visitVarInsn(ALOAD, 0);
+ transformer.visitLdcInsn(reference.getMethodName());
+ List<TypeReference> formalTypes = reference.getFormalTypes();
+ transformer.visitLdcInsn(formalTypes.size());
+ transformer.visitTypeInsn(ANEWARRAY, binaryName(String.class));
+ for (int i = 0; i < formalTypes.size(); i++) {
+ transformer.visitInsn(DUP);
+ transformer.visitLdcInsn(i);
+ transformer.visitLdcInsn(formalTypes.get(i).getDescriptor());
+ transformer.visitInsn(AASTORE);
+ }
+ if (reference.getReturnType() != null) {
+ transformer.visitLdcInsn(reference.getReturnType().getDescriptor());
+ } else {
+ transformer.visitInsn(ACONST_NULL);
+ }
+ transformer.visitLdcInsn(apiLevel.getLevel());
+ transformer.visitVarInsn(ALOAD, 1);
+ transformer.visitMethodInsn(
+ INVOKEVIRTUAL,
+ binaryName(AndroidApiClass.class),
+ "visitMethod",
+ methodDescriptor(
+ TraversalContinuation.class,
+ String.class,
+ String[].class,
+ String.class,
+ int.class,
+ BiFunction.class),
+ false);
+ // Note that instead of storing the result here, we dup it on the stack.
+ transformer.visitInsn(DUP);
+ transformer.visitMethodInsn(
+ INVOKEVIRTUAL,
+ binaryName(TraversalContinuation.class),
+ shouldBreak.getName(),
+ methodDescriptor(shouldBreak),
+ false);
+ Label label = new Label();
+ transformer.visitJumpInsn(IFEQ, label);
+ transformer.visitInsn(ARETURN);
+ transformer.visitLabel(label);
+ transformer.visitFrame(
+ F_SAME1,
+ 0,
+ new Object[] {},
+ 1,
+ new Object[] {binaryName(TraversalContinuation.class)});
+ // The pop here is needed to remove the dupped value in the case we do not
+ // return.
+ transformer.visitInsn(POP);
+ });
+ });
+ });
+ }
+
+ // The transformer below changes AndroidApiDatabasePackageTemplate.buildClass from:
+ // placeHolder();
+ // return null;
+ // into
+ // if ("<simple_class1>".equals(className)) {
+ // return new AndroidApiClassForClass_class_name1();
+ // }
+ // if ("<simple_class2>".equals(className)) {
+ // return new AndroidApiClassForClass_class_name2();
+ // }
+ // ...
+ // return null;
+ private static MethodTransformer getBuildClassTransformer(List<ParsedApiClass> classesForPackage)
+ throws NoSuchMethodException {
+ Method equals = Object.class.getMethod("equals", Object.class);
+ return replaceCode(
+ "placeHolder",
+ transformer -> {
+ classesForPackage.forEach(
+ apiClass -> {
+ transformer.visitLdcInsn(
+ DescriptorUtils.getSimpleClassNameFromDescriptor(
+ apiClass.getClassReference().getDescriptor()));
+ transformer.visitVarInsn(ALOAD, 0);
+ transformer.visitMethodInsn(
+ INVOKEVIRTUAL,
+ binaryName(String.class),
+ equals.getName(),
+ methodDescriptor(equals),
+ false);
+ Label label = new Label();
+ transformer.visitJumpInsn(IFEQ, label);
+ String binaryName =
+ DescriptorUtils.getBinaryNameFromDescriptor(getApiClassDescriptor(apiClass));
+ transformer.visitTypeInsn(NEW, binaryName);
+ transformer.visitInsn(DUP);
+ transformer.visitMethodInsn(INVOKESPECIAL, binaryName, "<init>", "()V", false);
+ transformer.visitInsn(ARETURN);
+ transformer.visitLabel(label);
+ transformer.visitFrame(Opcodes.F_SAME, 0, new Object[] {}, 0, new Object[0]);
+ });
+ });
+ }
+
+ // The transformer below changes AndroidApiDatabaseBuilderTemplate.buildClass from:
+ // String descriptor = classReference.getDescriptor();
+ // String packageName = DescriptorUtils.getPackageNameFromDescriptor(descriptor);
+ // String simpleClassName = DescriptorUtils.getSimpleClassNameFromDescriptor(descriptor);
+ // placeHolderForBuildClass();
+ // return null;
+ // into
+ // String descriptor = classReference.getDescriptor();
+ // String packageName = DescriptorUtils.getPackageNameFromDescriptor(descriptor);
+ // String simpleClassName = DescriptorUtils.getSimpleClassNameFromDescriptor(descriptor);
+ // if ("<package_name1>".equals(packageName)) {
+ // return AndroidApiClassForPackage_package_name1(simpleClassName);
+ // }
+ // if ("<package_name2>".equals(simpleClassName)) {
+ // return AndroidApiClassForPackage_package_name2(simpleClassName);
+ // }
+ // ...
+ // return null;
+ private static MethodTransformer getBuildPackageTransformer(List<String> packages)
+ throws NoSuchMethodException {
+ Method equals = String.class.getMethod("equals", Object.class);
+ Method buildClass =
+ AndroidApiDatabasePackageTemplate.class.getMethod("buildClass", String.class);
+ return replaceCode(
+ "placeHolderForBuildClass",
+ transformer -> {
+ packages.forEach(
+ pkg -> {
+ transformer.visitLdcInsn(pkg);
+ transformer.visitVarInsn(ALOAD, 2);
+ transformer.visitMethodInsn(
+ INVOKEVIRTUAL,
+ binaryName(String.class),
+ equals.getName(),
+ methodDescriptor(equals),
+ false);
+ Label label = new Label();
+ transformer.visitJumpInsn(IFEQ, label);
+ transformer.visitVarInsn(ALOAD, 3);
+ transformer.visitMethodInsn(
+ INVOKESTATIC,
+ DescriptorUtils.getBinaryNameFromDescriptor(getPackageBuilderDescriptor(pkg)),
+ buildClass.getName(),
+ methodDescriptor(buildClass),
+ false);
+ transformer.visitInsn(ARETURN);
+ transformer.visitLabel(label);
+ transformer.visitFrame(
+ Opcodes.F_FULL,
+ 4,
+ new Object[] {
+ binaryName(ClassReference.class),
+ binaryName(String.class),
+ binaryName(String.class),
+ binaryName(String.class)
+ },
+ 0,
+ new Object[0]);
+ });
+ });
+ }
+
+ // The transformer below changes AndroidApiDatabaseBuilderTemplate.buildClass from:
+ // placeHolderForVisitApiClasses();
+ // into
+ // classDescriptorConsumer.accept("<descriptor_class_1>");
+ // classDescriptorConsumer.accept("<descriptor_class_2>");
+ // ...
+ // return null;
+ private static MethodTransformer getVisitApiClassesTransformer(List<ParsedApiClass> apiClasses) {
+ return replaceCode(
+ "placeHolderForVisitApiClasses",
+ transformer -> {
+ apiClasses.forEach(
+ apiClass -> {
+ transformer.visitVarInsn(ALOAD, 0);
+ transformer.visitLdcInsn(apiClass.getClassReference().getDescriptor());
+ transformer.visitMethodInsn(
+ INVOKEINTERFACE,
+ binaryName(Consumer.class),
+ "accept",
+ "(Ljava/lang/Object;)V",
+ true);
+ });
+ });
+ }
+
+ private static MethodTransformer replaceCode(
+ String placeholderName, Consumer<MethodTransformer> consumer) {
+ return new MethodTransformer() {
+
+ @Override
+ public void visitMaxs(int maxStack, int maxLocals) {
+ super.visitMaxs(-1, maxLocals);
+ }
+
+ @Override
+ public void visitMethodInsn(
+ int opcode, String owner, String name, String descriptor, boolean isInterface) {
+ if (name.equals(placeholderName)) {
+ consumer.accept(this);
+ } else {
+ super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
+ }
+ }
+ };
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/apimodel/AndroidApiDatabaseBuilderGeneratorTest.java b/src/test/java/com/android/tools/r8/apimodel/AndroidApiDatabaseBuilderGeneratorTest.java
new file mode 100644
index 0000000..c47353e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodel/AndroidApiDatabaseBuilderGeneratorTest.java
@@ -0,0 +1,272 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.apimodel;
+
+import static com.android.tools.r8.apimodel.AndroidApiDatabaseBuilderGenerator.generatedMainDescriptor;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.JvmTestRunResult;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.apimodel.AndroidApiVersionsXmlParser.ParsedApiClass;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.BooleanBox;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.IntBox;
+import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.TraversalContinuation;
+import com.android.tools.r8.utils.ZipUtils.ZipBuilder;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.BiConsumer;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class AndroidApiDatabaseBuilderGeneratorTest extends TestBase {
+
+ protected final TestParameters parameters;
+ private static final Path API_VERSIONS_XML =
+ Paths.get(ToolHelper.THIRD_PARTY_DIR, "android_jar", "api-versions", "api-versions.xml");
+ private static final AndroidApiLevel API_LEVEL = AndroidApiLevel.R;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withSystemRuntime().build();
+ }
+
+ public AndroidApiDatabaseBuilderGeneratorTest(TestParameters parameters) {
+ this.parameters = parameters;
+ }
+
+ private static Path generateJar() throws Exception {
+ return generateJar(
+ AndroidApiVersionsXmlParser.getParsedApiClasses(API_VERSIONS_XML.toFile(), API_LEVEL));
+ }
+
+ private static Path generateJar(List<ParsedApiClass> apiClasses) throws Exception {
+ TemporaryFolder temp = new TemporaryFolder();
+ temp.create();
+ ZipBuilder builder = ZipBuilder.builder(temp.newFile("out.jar").toPath());
+ AndroidApiDatabaseBuilderGenerator.generate(
+ apiClasses,
+ (descriptor, content) -> {
+ try {
+ String binaryName = DescriptorUtils.getBinaryNameFromDescriptor(descriptor) + ".class";
+ builder.addBytes(binaryName, content);
+ } catch (IOException exception) {
+ throw new RuntimeException(exception);
+ }
+ });
+ return builder.build();
+ }
+
+ public static void main(String[] args) throws Exception {
+ generateJar();
+ }
+
+ @Test
+ public void testDatabaseGenerationUpToDate() {
+ assumeTrue("b/190368382", false);
+ }
+
+ @Test
+ public void testCanParseApiVersionsXml() throws Exception {
+ // This tests makes a rudimentary check on the number of classes, fields and methods in
+ // api-versions.xml to ensure that the runtime tests do not vacuously succeed.
+ List<ParsedApiClass> parsedApiClasses =
+ AndroidApiVersionsXmlParser.getParsedApiClasses(API_VERSIONS_XML.toFile(), API_LEVEL);
+ IntBox numberOfFields = new IntBox(0);
+ IntBox numberOfMethods = new IntBox(0);
+ parsedApiClasses.forEach(
+ apiClass -> {
+ apiClass.visitFieldReferences(
+ ((apiLevel, fieldReferences) -> {
+ fieldReferences.forEach(field -> numberOfFields.increment());
+ }));
+ apiClass.visitMethodReferences(
+ ((apiLevel, methodReferences) -> {
+ methodReferences.forEach(field -> numberOfMethods.increment());
+ }));
+ });
+ // These numbers will change when updating api-versions.xml
+ assertEquals(4742, parsedApiClasses.size());
+ assertEquals(25144, numberOfFields.get());
+ assertEquals(38661, numberOfMethods.get());
+ }
+
+ @Test()
+ public void testGeneratedOutputForVisitClasses() throws Exception {
+ runTest(
+ TestGeneratedMainVisitClasses.class,
+ (parsedApiClasses, runResult) -> {
+ String expectedOutput =
+ StringUtils.lines(
+ ListUtils.map(
+ parsedApiClasses, apiClass -> apiClass.getClassReference().getDescriptor()));
+ runResult.assertSuccessWithOutput(expectedOutput);
+ });
+ }
+
+ @Test()
+ public void testBuildClassesContinue() throws Exception {
+ runTest(
+ TestBuildClassesContinue.class,
+ (parsedApiClasses, runResult) -> {
+ runResult.assertSuccessWithOutputLines(getExpected(parsedApiClasses, false));
+ });
+ }
+
+ @Test()
+ public void testBuildClassesBreak() throws Exception {
+ runTest(
+ TestBuildClassesBreak.class,
+ (parsedApiClasses, runResult) -> {
+ runResult.assertSuccessWithOutputLines(getExpected(parsedApiClasses, true));
+ });
+ }
+
+ private void runTest(
+ Class<?> testClass, BiConsumer<List<ParsedApiClass>, JvmTestRunResult> resultConsumer)
+ throws Exception {
+ List<ParsedApiClass> parsedApiClasses =
+ AndroidApiVersionsXmlParser.getParsedApiClasses(API_VERSIONS_XML.toFile(), API_LEVEL);
+ testForJvm()
+ .addProgramClassFileData(
+ transformer(testClass)
+ .replaceClassDescriptorInMethodInstructions(
+ descriptor(AndroidApiDatabaseBuilderTemplate.class), generatedMainDescriptor())
+ .transform())
+ .addLibraryFiles(generateJar(parsedApiClasses))
+ // TODO(b/190368382): This will change when databasebuilder is included in deps.
+ .addLibraryFiles(ToolHelper.R8_WITHOUT_DEPS_JAR, ToolHelper.DEPS)
+ .addDefaultRuntimeLibrary(parameters)
+ .run(parameters.getRuntime(), testClass)
+ .apply(
+ result -> {
+ if (result.getExitCode() != 0) {
+ System.out.println(result.getStdErr());
+ }
+ })
+ .assertSuccess()
+ .apply(result -> resultConsumer.accept(parsedApiClasses, result));
+ }
+
+ private List<String> getExpected(List<ParsedApiClass> parsedApiClasses, boolean abort) {
+ List<String> expected = new ArrayList<>();
+ parsedApiClasses.forEach(
+ apiClass -> {
+ expected.add(apiClass.getClassReference().getDescriptor());
+ expected.add(apiClass.getApiLevel().getName());
+ BooleanBox added = new BooleanBox(false);
+ apiClass.visitFieldReferences(
+ (apiLevel, fieldReferences) -> {
+ fieldReferences.forEach(
+ fieldReference -> {
+ if (added.isTrue() && abort) {
+ return;
+ }
+ added.set();
+ expected.add(fieldReference.getFieldType().getDescriptor());
+ expected.add(fieldReference.getFieldName());
+ expected.add(apiLevel.getName());
+ });
+ });
+ added.set(false);
+ apiClass.visitMethodReferences(
+ (apiLevel, methodReferences) -> {
+ methodReferences.forEach(
+ methodReference -> {
+ if (added.isTrue() && abort) {
+ return;
+ }
+ added.set();
+ expected.add(methodReference.getMethodDescriptor());
+ expected.add(methodReference.getMethodName());
+ expected.add(apiLevel.getName());
+ });
+ });
+ });
+ return expected;
+ }
+
+ public static class TestGeneratedMainVisitClasses {
+
+ public static void main(String[] args) {
+ AndroidApiDatabaseBuilderTemplate.visitApiClasses(System.out::println);
+ }
+ }
+
+ public static class TestBuildClassesContinue {
+
+ public static void main(String[] args) {
+ AndroidApiDatabaseBuilderTemplate.visitApiClasses(
+ descriptor -> {
+ com.android.tools.r8.androidapi.AndroidApiClass apiClass =
+ AndroidApiDatabaseBuilderTemplate.buildClass(
+ Reference.classFromDescriptor(descriptor));
+ if (apiClass != null) {
+ System.out.println(descriptor);
+ System.out.println(apiClass.getApiLevel().getName());
+ apiClass.visitFields(
+ (reference, apiLevel) -> {
+ System.out.println(reference.getFieldType().getDescriptor());
+ System.out.println(reference.getFieldName());
+ System.out.println(apiLevel.getName());
+ return TraversalContinuation.CONTINUE;
+ });
+ apiClass.visitMethods(
+ (reference, apiLevel) -> {
+ System.out.println(reference.getMethodDescriptor());
+ System.out.println(reference.getMethodName());
+ System.out.println(apiLevel.getName());
+ return TraversalContinuation.CONTINUE;
+ });
+ }
+ });
+ }
+ }
+
+ public static class TestBuildClassesBreak {
+
+ public static void main(String[] args) {
+ AndroidApiDatabaseBuilderTemplate.visitApiClasses(
+ descriptor -> {
+ com.android.tools.r8.androidapi.AndroidApiClass apiClass =
+ AndroidApiDatabaseBuilderTemplate.buildClass(
+ Reference.classFromDescriptor(descriptor));
+ if (apiClass != null) {
+ System.out.println(descriptor);
+ System.out.println(apiClass.getApiLevel().getName());
+ apiClass.visitFields(
+ (reference, apiLevel) -> {
+ System.out.println(reference.getFieldType().getDescriptor());
+ System.out.println(reference.getFieldName());
+ System.out.println(apiLevel.getName());
+ return TraversalContinuation.BREAK;
+ });
+ apiClass.visitMethods(
+ (reference, apiLevel) -> {
+ System.out.println(reference.getMethodDescriptor());
+ System.out.println(reference.getMethodName());
+ System.out.println(apiLevel.getName());
+ return TraversalContinuation.BREAK;
+ });
+ }
+ });
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/apimodel/AndroidApiDatabaseBuilderTemplate.java b/src/test/java/com/android/tools/r8/apimodel/AndroidApiDatabaseBuilderTemplate.java
new file mode 100644
index 0000000..4b21f6e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodel/AndroidApiDatabaseBuilderTemplate.java
@@ -0,0 +1,32 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.apimodel;
+
+import com.android.tools.r8.androidapi.AndroidApiClass;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.utils.DescriptorUtils;
+import java.util.function.Consumer;
+
+/** This is a template for generating the AndroidApiDatabaseBuilder. */
+public class AndroidApiDatabaseBuilderTemplate /* AndroidApiDatabaseBuilder */ {
+
+ public static void visitApiClasses(Consumer<String> classDescriptorConsumer) {
+ // Code added dynamically in AndroidApiDatabaseBuilderGenerator.
+ placeHolderForVisitApiClasses();
+ }
+
+ public static AndroidApiClass buildClass(ClassReference classReference) {
+ String descriptor = classReference.getDescriptor();
+ String packageName = DescriptorUtils.getPackageNameFromDescriptor(descriptor);
+ String simpleClassName = DescriptorUtils.getSimpleClassNameFromDescriptor(descriptor);
+ // Code added dynamically in AndroidApiDatabaseBuilderGenerator.
+ placeHolderForBuildClass();
+ return null;
+ }
+
+ private static void placeHolderForVisitApiClasses() {}
+
+ private static void placeHolderForBuildClass() {}
+}
diff --git a/src/test/java/com/android/tools/r8/apimodel/AndroidApiDatabaseClassTemplate.java b/src/test/java/com/android/tools/r8/apimodel/AndroidApiDatabaseClassTemplate.java
new file mode 100644
index 0000000..2598898
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodel/AndroidApiDatabaseClassTemplate.java
@@ -0,0 +1,56 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.apimodel;
+
+import com.android.tools.r8.androidapi.AndroidApiClass;
+import com.android.tools.r8.references.FieldReference;
+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.TraversalContinuation;
+import java.util.function.BiFunction;
+
+/** This is a template for generating AndroidApiDatabaseClass extending AndroidApiClass */
+public class AndroidApiDatabaseClassTemplate extends AndroidApiClass {
+
+ protected AndroidApiDatabaseClassTemplate() {
+ // Code added dynamically in AndroidApiDatabaseBuilderGenerator.
+ super(Reference.classFromDescriptor(placeHolderForInit()));
+ }
+
+ @Override
+ public AndroidApiLevel getApiLevel() {
+ // Code added dynamically in AndroidApiDatabaseBuilderGenerator.
+ return placeHolderForGetApiLevel();
+ }
+
+ @Override
+ public TraversalContinuation visitFields(
+ BiFunction<FieldReference, AndroidApiLevel, TraversalContinuation> visitor) {
+ // Code added dynamically in AndroidApiDatabaseBuilderGenerator.
+ placeHolderForVisitFields();
+ return TraversalContinuation.CONTINUE;
+ }
+
+ @Override
+ public TraversalContinuation visitMethods(
+ BiFunction<MethodReference, AndroidApiLevel, TraversalContinuation> visitor) {
+ // Code added dynamically in AndroidApiDatabaseBuilderGenerator.
+ placeHolderForVisitMethods();
+ return TraversalContinuation.CONTINUE;
+ }
+
+ private static String placeHolderForInit() {
+ return null;
+ }
+
+ private static AndroidApiLevel placeHolderForGetApiLevel() {
+ return null;
+ }
+
+ private static void placeHolderForVisitFields() {}
+
+ private static void placeHolderForVisitMethods() {}
+}
diff --git a/src/test/java/com/android/tools/r8/apimodel/AndroidApiDatabasePackageTemplate.java b/src/test/java/com/android/tools/r8/apimodel/AndroidApiDatabasePackageTemplate.java
new file mode 100644
index 0000000..3919d11
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodel/AndroidApiDatabasePackageTemplate.java
@@ -0,0 +1,19 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.apimodel;
+
+import com.android.tools.r8.androidapi.AndroidApiClass;
+
+/** This is a template for generating the AndroidApiDatabasePackage. */
+public class AndroidApiDatabasePackageTemplate {
+
+ public static AndroidApiClass buildClass(String className) {
+ // Code added dynamically in AndroidApiDatabaseBuilderGenerator.
+ placeHolder();
+ return null;
+ }
+
+ private static void placeHolder() {}
+}
diff --git a/src/test/java/com/android/tools/r8/apimodel/AndroidApiVersionsXmlParser.java b/src/test/java/com/android/tools/r8/apimodel/AndroidApiVersionsXmlParser.java
new file mode 100644
index 0000000..c904dff
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodel/AndroidApiVersionsXmlParser.java
@@ -0,0 +1,175 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.apimodel;
+
+import static com.android.tools.r8.utils.FunctionUtils.ignoreArgument;
+
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.FieldReference;
+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.DescriptorUtils;
+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 java.io.File;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.function.BiConsumer;
+import javax.xml.parsers.DocumentBuilderFactory;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+public class AndroidApiVersionsXmlParser {
+
+ private final List<ParsedApiClass> classes = new ArrayList<>();
+
+ private final File apiVersionsXml;
+ private final AndroidApiLevel maxApiLevel;
+
+ private AndroidApiVersionsXmlParser(File apiVersionsXml, AndroidApiLevel maxApiLevel) {
+ this.apiVersionsXml = apiVersionsXml;
+ this.maxApiLevel = maxApiLevel;
+ }
+
+ private ParsedApiClass register(ClassReference reference, AndroidApiLevel apiLevel) {
+ ParsedApiClass parsedApiClass = new ParsedApiClass(reference, apiLevel);
+ classes.add(parsedApiClass);
+ return parsedApiClass;
+ }
+
+ private void readApiVersionsXmlFile() throws Exception {
+ CodeInspector inspector = new CodeInspector(ToolHelper.getAndroidJar(maxApiLevel));
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ Document document = factory.newDocumentBuilder().parse(apiVersionsXml);
+ NodeList classes = document.getElementsByTagName("class");
+ 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);
+ String type = DescriptorUtils.getJavaTypeFromBinaryName(getName(node));
+ ClassSubject clazz = inspector.clazz(type);
+ if (!clazz.isPresent()) {
+ // TODO(b/190326408): Investigate why the class is not present.
+ continue;
+ }
+ ClassReference originalReference = clazz.getOriginalReference();
+ ParsedApiClass parsedApiClass = register(originalReference, apiLevel);
+ NodeList members = node.getChildNodes();
+ for (int j = 0; j < members.getLength(); j++) {
+ Node memberNode = members.item(j);
+ if (isMethod(memberNode)) {
+ // TODO(b/190326408): Check for existence.
+ parsedApiClass.register(
+ getMethodReference(originalReference, memberNode),
+ getMaxAndroidApiLevelFromNode(memberNode, apiLevel));
+ } else if (isField(memberNode)) {
+ // The field do not have descriptors and are supposed to be unique.
+ FieldSubject fieldSubject = clazz.uniqueFieldWithName(getName(memberNode));
+ if (!fieldSubject.isPresent()) {
+ // TODO(b/190326408): Investigate why the member is not present.
+ continue;
+ }
+ parsedApiClass.register(
+ fieldSubject.getOriginalReference(),
+ getMaxAndroidApiLevelFromNode(memberNode, apiLevel));
+ }
+ }
+ }
+ }
+
+ private boolean isMethod(Node node) {
+ return node.getNodeName().equals("method");
+ }
+
+ private String getName(Node node) {
+ return node.getAttributes().getNamedItem("name").getNodeValue();
+ }
+
+ private MethodReference getMethodReference(ClassReference classDescriptor, Node node) {
+ assert isMethod(node);
+ String name = getName(node);
+ int signatureStart = name.indexOf('(');
+ assert signatureStart > 0;
+ String parsedName = name.substring(0, signatureStart).replace("<", "<");
+ assert !parsedName.contains("&");
+ return Reference.methodFromDescriptor(
+ classDescriptor.getDescriptor(), parsedName, name.substring(signatureStart));
+ }
+
+ private boolean isField(Node node) {
+ return node.getNodeName().equals("field");
+ }
+
+ private AndroidApiLevel getMaxAndroidApiLevelFromNode(Node node, AndroidApiLevel defaultValue) {
+ if (node == null) {
+ return defaultValue;
+ }
+ Node since = node.getAttributes().getNamedItem("since");
+ if (since == null) {
+ return defaultValue;
+ }
+ return defaultValue.max(
+ AndroidApiLevel.getAndroidApiLevel(Integer.parseInt(since.getNodeValue())));
+ }
+
+ public static List<ParsedApiClass> getParsedApiClasses(
+ File apiVersionsXml, AndroidApiLevel apiLevel) throws Exception {
+ AndroidApiVersionsXmlParser parser = new AndroidApiVersionsXmlParser(apiVersionsXml, apiLevel);
+ parser.readApiVersionsXmlFile();
+ return parser.classes;
+ }
+
+ public static class ParsedApiClass {
+
+ private final ClassReference classReference;
+ private final AndroidApiLevel apiLevel;
+ private final TreeMap<AndroidApiLevel, List<FieldReference>> fieldReferences = new TreeMap<>();
+ private final Map<AndroidApiLevel, List<MethodReference>> methodReferences = new TreeMap<>();
+
+ public ClassReference getClassReference() {
+ return classReference;
+ }
+
+ public AndroidApiLevel getApiLevel() {
+ return apiLevel;
+ }
+
+ private ParsedApiClass(ClassReference classReference, AndroidApiLevel apiLevel) {
+ this.classReference = classReference;
+ this.apiLevel = apiLevel;
+ }
+
+ private void register(FieldReference reference, AndroidApiLevel apiLevel) {
+ fieldReferences.computeIfAbsent(apiLevel, ignoreArgument(ArrayList::new)).add(reference);
+ }
+
+ private void register(MethodReference reference, AndroidApiLevel apiLevel) {
+ methodReferences.computeIfAbsent(apiLevel, ignoreArgument(ArrayList::new)).add(reference);
+ }
+
+ public void visitFieldReferences(BiConsumer<AndroidApiLevel, List<FieldReference>> consumer) {
+ fieldReferences.forEach(
+ (apiLevel, references) -> {
+ references.sort(Comparator.comparing(FieldReference::getFieldName));
+ consumer.accept(apiLevel, references);
+ });
+ }
+
+ public void visitMethodReferences(BiConsumer<AndroidApiLevel, List<MethodReference>> consumer) {
+ methodReferences.forEach(
+ (apiLevel, references) -> {
+ references.sort(Comparator.comparing(MethodReference::getMethodName));
+ consumer.accept(apiLevel, references);
+ });
+ }
+ }
+}