Refactor API database generation to production code.
Bug: b/510269018
Change-Id: Ia38db8f52d798855dc8012e036a5d5de8950d8ca
diff --git a/src/main/java/com/android/tools/r8/androidapi/ApiDatabaseEntry.java b/src/main/java/com/android/tools/r8/androidapi/ApiDatabaseEntry.java
index ba19aa9..3c5c80e 100644
--- a/src/main/java/com/android/tools/r8/androidapi/ApiDatabaseEntry.java
+++ b/src/main/java/com/android/tools/r8/androidapi/ApiDatabaseEntry.java
@@ -11,9 +11,10 @@
import com.android.tools.r8.graph.DexMethod;
import com.android.tools.r8.graph.DexReference;
import com.android.tools.r8.graph.DexString;
-import com.android.tools.r8.graph.DexType;
+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.utils.internal.ThrowingFunction;
-import com.android.tools.r8.utils.internal.exceptions.Unreachable;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
@@ -26,7 +27,7 @@
* pooled in a constant pool (see {@link ConstantPoolEntry}).
*
* <p>Each entry can be serialized into a "unique descriptor" which is a compact byte array
- * containing identifiers and constant pool indices. See {@code
+ * containing identifiers and constant pool indices. See {@link
* com.android.tools.r8.apimodel.AndroidApiHashingDatabaseBuilderGenerator} for the serialization
* format.
*/
@@ -48,8 +49,6 @@
ThrowingFunction<ConstantPoolEntry, Integer, IOException> constantPoolLookup)
throws IOException;
- public abstract ApiDatabaseEntry withHolder(DexType newHolder);
-
@Override
public final int hashCode() {
return apiDatabaseHashCode();
@@ -85,6 +84,29 @@
DexString.encodeToMutf8(reference.getFieldName()));
}
+ public static ApiDatabaseEntry of(ClassReference reference) {
+ return new TypeEntry(DexString.encodeToMutf8(reference.getDescriptor()));
+ }
+
+ public static ApiDatabaseEntry of(MethodReference reference) {
+ byte[][] parameters = new byte[reference.getFormalTypes().size()][];
+ for (int i = 0; i < parameters.length; i++) {
+ parameters[i] = DexString.encodeToMutf8(reference.getFormalTypes().get(i).getDescriptor());
+ }
+ return new MethodEntry(
+ DexString.encodeToMutf8(reference.getHolderClass().getDescriptor()),
+ DexString.encodeToMutf8(reference.getMethodName()),
+ parameters,
+ DexString.encodeToMutf8(
+ reference.getReturnType() == null ? "V" : reference.getReturnType().getDescriptor()));
+ }
+
+ public static ApiDatabaseEntry of(FieldReference reference) {
+ return new FieldEntry(
+ DexString.encodeToMutf8(reference.getHolderClass().getDescriptor()),
+ DexString.encodeToMutf8(reference.getFieldName()));
+ }
+
private static byte getFirstByteFromShort(int value) {
assert isU2(value);
return (byte) (value >> 8);
@@ -159,10 +181,6 @@
};
}
- @Override
- public ApiDatabaseEntry withHolder(DexType newHolder) {
- throw new Unreachable();
- }
@Override
public boolean equals(Object obj) {
@@ -223,10 +241,6 @@
return baos.toByteArray();
}
- @Override
- public ApiDatabaseEntry withHolder(DexType newHolder) {
- throw new Unreachable();
- }
@Override
public boolean equals(Object obj) {
@@ -279,10 +293,6 @@
};
}
- @Override
- public ApiDatabaseEntry withHolder(DexType newHolder) {
- return new FieldEntry(newHolder.getDescriptor().content, this.name);
- }
@Override
public boolean equals(Object obj) {
diff --git a/src/main/java/com/android/tools/r8/apimodel/AndroidApiHashingDatabaseBuilderGenerator.java b/src/main/java/com/android/tools/r8/apimodel/AndroidApiHashingDatabaseBuilderGenerator.java
new file mode 100644
index 0000000..8f889e8
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/apimodel/AndroidApiHashingDatabaseBuilderGenerator.java
@@ -0,0 +1,359 @@
+// 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.androidapi.AndroidApiDataAccess.constantPoolHash;
+import static com.android.tools.r8.lightir.ByteUtils.isU2;
+import static com.android.tools.r8.lightir.ByteUtils.setBitAtIndex;
+import static com.android.tools.r8.utils.internal.MapUtils.ignoreKey;
+
+import com.android.tools.r8.androidapi.AndroidApiDataAccess;
+import com.android.tools.r8.androidapi.ApiDatabaseEntry;
+import com.android.tools.r8.androidapi.ApiDatabaseEntry.ConstantPoolEntry;
+import com.android.tools.r8.references.ClassReference;
+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.internal.IntBox;
+import com.android.tools.r8.utils.internal.ThrowingBiConsumer;
+import com.android.tools.r8.utils.internal.collections.Pair;
+import it.unimi.dsi.fastutil.ints.Int2ObjectLinkedOpenHashMap;
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
+import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenHashMap;
+import it.unimi.dsi.fastutil.objects.Object2IntMap;
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+public class AndroidApiHashingDatabaseBuilderGenerator {
+
+ public static class GenerationException extends Exception {
+ public GenerationException(String message) {
+ super(message);
+ }
+
+ public GenerationException(String message, Throwable cause) {
+ super(message, cause);
+ }
+ }
+
+ /** The returned map has hash-independent iteration. */
+ public static Map<ApiDatabaseEntry, AndroidApiLevel> generateEntries(
+ List<ParsedApiClass> apiClasses) throws GenerationException {
+ Map<ClassReference, ParsedApiClass> lookupMap = new HashMap<>();
+ Map<ApiDatabaseEntry, AndroidApiLevel> databaseEntries = new LinkedHashMap<>();
+
+ for (ParsedApiClass apiClass : apiClasses) {
+ lookupMap.put(apiClass.getClassReference(), apiClass);
+ databaseEntries.put(
+ ApiDatabaseEntry.of(apiClass.getClassReference()), apiClass.getApiLevel());
+ }
+
+ for (ParsedApiClass apiClass : apiClasses) {
+ computeAllReferencesInHierarchy(
+ lookupMap, apiClass.getClassReference(), apiClass, AndroidApiLevel.B, databaseEntries);
+ }
+
+ return databaseEntries;
+ }
+
+ /**
+ * This method will generate one single database file where the format is as follows (uX is X
+ * number of unsigned bytes):
+ *
+ * <pre>
+ * constant_pool_size: u4
+ * constant_pool: [constant_pool_size * payload_entry]
+ * constant_pool_map: [0..max_hash(ConstantPoolEntry) * payload_entry]
+ * api_map: [0..max_hash(ApiDatabaseEntry) * payload_entry]
+ * payload raw data.
+ *
+ * payload_entry: u4:relative_offset_from_payload_start_or_tagged_value + u2:length
+ * </pre>
+ *
+ * For hash_definitions and entries see {@link AndroidApiDataAccess}.
+ */
+ public static void writeEntries(
+ Map<ApiDatabaseEntry, AndroidApiLevel> databaseEntries, Path pathToApiLevels)
+ throws GenerationException {
+ try (FileOutputStream fileOutputStream = new FileOutputStream(pathToApiLevels.toFile())) {
+ DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream);
+ generateDatabase(databaseEntries, dataOutputStream);
+ } catch (IOException e) {
+ throw new GenerationException("Failed to write API database to " + pathToApiLevels, e);
+ }
+ }
+
+ private static class ConstantPool {
+
+ private final IntBox intBox = new IntBox(0);
+ private final Object2IntMap<ConstantPoolEntry> pool = new Object2IntLinkedOpenHashMap<>();
+
+ public int getOrAdd(ConstantPoolEntry entry) {
+ return pool.computeIfAbsent(entry, ignored -> intBox.getAndIncrement());
+ }
+
+ public void forEach(ThrowingBiConsumer<ConstantPoolEntry, Integer, IOException> consumer)
+ throws IOException {
+ for (var mapEntry : pool.object2IntEntrySet()) {
+ consumer.accept(mapEntry.getKey(), mapEntry.getIntValue());
+ }
+ }
+
+ public int size() {
+ return pool.size();
+ }
+ }
+
+ private static int setUniqueConstantPoolEntry(int id) {
+ return setBitAtIndex(id, 32);
+ }
+
+ private static void generateDatabase(
+ Map<ApiDatabaseEntry, AndroidApiLevel> databaseEntries, DataOutputStream outputStream)
+ throws GenerationException, IOException {
+ Int2ObjectMap<List<Pair<ApiDatabaseEntry, AndroidApiLevel>>> generationMap =
+ new Int2ObjectLinkedOpenHashMap<>();
+ ConstantPool constantPool = new ConstantPool();
+
+ int constantPoolHashMapSize = 1 << AndroidApiDataAccess.entrySizeInBitsForConstantPoolMap();
+ int apiHashMapSize = 1 << AndroidApiDataAccess.entrySizeInBitsForApiLevelMap();
+
+ for (Entry<ApiDatabaseEntry, AndroidApiLevel> entry : databaseEntries.entrySet()) {
+ int newCode = AndroidApiDataAccess.apiLevelHash(entry.getKey());
+ if (newCode < 0 || newCode > apiHashMapSize) {
+ throw new GenerationException("Hash code out of bounds: " + newCode);
+ }
+ generationMap
+ .computeIfAbsent(newCode, ignoreKey(ArrayList::new))
+ .add(Pair.create(entry.getKey(), entry.getValue()));
+ }
+
+ Set<String> uniqueHashes = new HashSet<>();
+ Int2ObjectMap<Pair<Integer, Integer>> offsetMap = new Int2ObjectLinkedOpenHashMap<>();
+ ByteArrayOutputStream payload = new ByteArrayOutputStream();
+
+ // Serialize api map into payload. This will also generate the entire needed constant pool.
+ for (Int2ObjectMap.Entry<List<Pair<ApiDatabaseEntry, AndroidApiLevel>>> entry :
+ generationMap.int2ObjectEntrySet()) {
+ int startingOffset = payload.size();
+ int length = serializeIntoPayload(entry.getValue(), payload, constantPool, uniqueHashes);
+ offsetMap.put(entry.getIntKey(), Pair.create(startingOffset, length));
+ }
+
+ // Write constant pool size <u4:size>.
+ outputStream.writeInt(constantPool.size());
+
+ // Write constant pool consisting of <u4:payload_offset><u2:length>.
+ if (outputStream.size() != AndroidApiDataAccess.constantPoolOffset()) {
+ throw new GenerationException(
+ "Unexpected constant pool offset: expected "
+ + AndroidApiDataAccess.constantPoolOffset()
+ + ", got "
+ + outputStream.size());
+ }
+ IntBox lastReadIndex = new IntBox(-1);
+ constantPool.forEach(
+ (entry, id) -> {
+ if (id <= lastReadIndex.getAndIncrement()) {
+ throw new IOException("Constant pool ID out of order");
+ }
+ outputStream.writeInt(payload.size());
+ outputStream.writeShort(entry.getLength());
+ entry.writeTo(payload);
+ });
+
+ // Serialize hash lookup table for constant pool.
+ Map<Integer, List<Integer>> constantPoolLookupTable = new HashMap<>();
+ constantPool.forEach(
+ (entry, id) -> {
+ int constantPoolHash = constantPoolHash(entry);
+ assert constantPoolHash >= 0 && constantPoolHash <= constantPoolHashMapSize;
+ constantPoolLookupTable
+ .computeIfAbsent(constantPoolHash, ignoreKey(ArrayList::new))
+ .add(id);
+ });
+
+ int[] constantPoolEntries = new int[constantPoolHashMapSize];
+ int[] constantPoolEntryLengths = new int[constantPoolHashMapSize];
+ for (Entry<Integer, List<Integer>> entry : constantPoolLookupTable.entrySet()) {
+ // Tag if we have a unique value.
+ if (entry.getValue().size() == 1) {
+ int id = entry.getValue().get(0);
+ constantPoolEntries[entry.getKey()] = setUniqueConstantPoolEntry(id);
+ } else {
+ constantPoolEntries[entry.getKey()] = payload.size();
+ ByteArrayOutputStream temp = new ByteArrayOutputStream();
+ for (Integer id : entry.getValue()) {
+ temp.write(intToShortEncodedByteArray(id));
+ }
+ payload.write(temp.toByteArray());
+ constantPoolEntryLengths[entry.getKey()] = temp.size();
+ }
+ }
+ // Write constant pool lookup entries consisting of <u4:payload_offset><u2:length>.
+ if (outputStream.size()
+ != AndroidApiDataAccess.constantPoolHashMapOffset(constantPool.size())) {
+ throw new GenerationException("Unexpected constant pool hash map offset");
+ }
+ for (int i = 0; i < constantPoolEntries.length; i++) {
+ outputStream.writeInt(constantPoolEntries[i]);
+ outputStream.writeShort(constantPoolEntryLengths[i]);
+ }
+
+ int[] apiOffsets = new int[apiHashMapSize];
+ int[] apiOffsetLengths = new int[apiHashMapSize];
+ for (Int2ObjectMap.Entry<Pair<Integer, Integer>> hashIndexAndOffset :
+ offsetMap.int2ObjectEntrySet()) {
+ if (apiOffsets[hashIndexAndOffset.getIntKey()] != 0) {
+ throw new GenerationException(
+ "Hash collision in API map at index " + hashIndexAndOffset.getIntKey());
+ }
+ Pair<Integer, Integer> value = hashIndexAndOffset.getValue();
+ int offset = value.getFirst();
+ int length = value.getSecond();
+ apiOffsets[hashIndexAndOffset.getKey()] = offset;
+ apiOffsetLengths[hashIndexAndOffset.getKey()] = length;
+ }
+
+ // Write api lookup entries consisting of <u4:payload_offset><u2:length>.
+ if (outputStream.size() != AndroidApiDataAccess.apiLevelHashMapOffset(constantPool.size())) {
+ throw new GenerationException("Unexpected api level hash map offset");
+ }
+ for (int i = 0; i < apiOffsets.length; i++) {
+ outputStream.writeInt(apiOffsets[i]);
+ outputStream.writeShort(apiOffsetLengths[i]);
+ }
+
+ // Write the payload.
+ outputStream.write(payload.toByteArray());
+ }
+
+ /** This will serialize a collection of DexReferences and apis into a byte stream. */
+ private static int serializeIntoPayload(
+ List<Pair<ApiDatabaseEntry, AndroidApiLevel>> pairs,
+ ByteArrayOutputStream payload,
+ ConstantPool constantPool,
+ Set<String> seen)
+ throws GenerationException, IOException {
+ ByteArrayOutputStream temp = new ByteArrayOutputStream();
+ for (Pair<ApiDatabaseEntry, AndroidApiLevel> pair : pairs) {
+ byte[] uniqueDescriptorForReference =
+ pair.getFirst().getUniqueDescriptor(constantPool::getOrAdd);
+ if (uniqueDescriptorForReference == ApiDatabaseEntry.getNonExistingDescriptor()) {
+ throw new GenerationException("Reference descriptor does not exist: " + pair.getFirst());
+ }
+ if (!seen.add(Arrays.toString(uniqueDescriptorForReference))) {
+ throw new GenerationException(
+ "Duplicate reference descriptor in payload: " + pair.getFirst());
+ }
+ temp.write(intToShortEncodedByteArray(uniqueDescriptorForReference.length));
+ temp.write(uniqueDescriptorForReference);
+ if (pair.getSecond() == AndroidApiLevel.MAIN
+ || pair.getSecond() == AndroidApiLevel.EXTENSION) {
+ throw new GenerationException("Invalid API level for database entry: " + pair.getSecond());
+ }
+ temp.write(pair.getSecond().serializeAsByte());
+ }
+ byte[] tempArray = temp.toByteArray();
+ payload.write(tempArray);
+ return tempArray.length;
+ }
+
+ public static byte[] intToShortEncodedByteArray(int value) {
+ assert isU2(value);
+ byte[] bytes = new byte[2];
+ bytes[0] = (byte) (value >> 8);
+ bytes[1] = (byte) value;
+ return bytes;
+ }
+
+ private static final String OBJECT_DESCRIPTOR = "Ljava/lang/Object;";
+
+ private static void computeAllReferencesInHierarchy(
+ Map<ClassReference, ParsedApiClass> lookupMap,
+ ClassReference holder,
+ ParsedApiClass apiClass,
+ AndroidApiLevel linkLevel,
+ Map<ApiDatabaseEntry, AndroidApiLevel> databaseEntries)
+ throws GenerationException {
+ if (!apiClass.getClassReference().getDescriptor().equals(OBJECT_DESCRIPTOR)) {
+ apiClass.forEachMethod(
+ (methodReference, apiLevel) -> {
+ MethodReference methodWithNewHolder =
+ Reference.method(
+ holder,
+ methodReference.getMethodName(),
+ methodReference.getFormalTypes(),
+ methodReference.getReturnType());
+ addIfNewOrApiLevelIsLower(
+ linkLevel, databaseEntries, apiLevel, ApiDatabaseEntry.of(methodWithNewHolder));
+ });
+ apiClass.forEachField(
+ (fieldReference, apiLevel) -> {
+ FieldTypelessReference fieldWithNewHolder =
+ new FieldTypelessReference(holder, fieldReference.getFieldName());
+ addIfNewOrApiLevelIsLower(
+ linkLevel, databaseEntries, apiLevel, ApiDatabaseEntry.of(fieldWithNewHolder));
+ });
+
+ apiClass.forEachSupertypeThrowing(
+ (superType, apiLevel) -> {
+ ParsedApiClass superApiClass = lookupMap.get(superType);
+ if (superApiClass == null) {
+ if (!superType.getDescriptor().equals(OBJECT_DESCRIPTOR)) {
+ throw new GenerationException(
+ "Supertype "
+ + superType.getTypeName()
+ + " of "
+ + apiClass.getClassReference().getTypeName()
+ + " is missing from the parsed API classes.");
+ }
+ } else {
+ computeAllReferencesInHierarchy(
+ lookupMap, holder, superApiClass, linkLevel.max(apiLevel), databaseEntries);
+ }
+ });
+
+ apiClass.forEachInterfaceThrowing(
+ (interfaceReference, apiLevel) -> {
+ ParsedApiClass interfaceApiClass = lookupMap.get(interfaceReference);
+ if (interfaceApiClass == null) {
+ throw new GenerationException(
+ "Interface "
+ + interfaceReference.getTypeName()
+ + " implemented by "
+ + apiClass.getClassReference().getTypeName()
+ + " is missing from the parsed API classes.");
+ }
+ computeAllReferencesInHierarchy(
+ lookupMap, holder, interfaceApiClass, linkLevel.max(apiLevel), databaseEntries);
+ });
+ }
+ }
+
+ private static void addIfNewOrApiLevelIsLower(
+ AndroidApiLevel linkLevel,
+ Map<ApiDatabaseEntry, AndroidApiLevel> additionMap,
+ AndroidApiLevel apiLevel,
+ ApiDatabaseEntry member) {
+ AndroidApiLevel currentApiLevel = apiLevel.max(linkLevel);
+ AndroidApiLevel existingApiLevel = additionMap.get(member);
+ if (existingApiLevel == null || currentApiLevel.isLessThanOrEqualTo(existingApiLevel)) {
+ additionMap.put(member, currentApiLevel);
+ }
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/apimodel/ParsedApiClass.java b/src/main/java/com/android/tools/r8/apimodel/ParsedApiClass.java
index 14ba584..2a32fda 100644
--- a/src/main/java/com/android/tools/r8/apimodel/ParsedApiClass.java
+++ b/src/main/java/com/android/tools/r8/apimodel/ParsedApiClass.java
@@ -7,6 +7,7 @@
import com.android.tools.r8.references.ClassReference;
import com.android.tools.r8.references.MethodReference;
import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.internal.ThrowingBiConsumer;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.BiConsumer;
@@ -47,6 +48,14 @@
supertypes.forEach(consumer);
}
+ /** Visited in insertion order. */
+ public <E extends Throwable> void forEachSupertypeThrowing(
+ ThrowingBiConsumer<ClassReference, AndroidApiLevel, E> consumer) throws E {
+ for (Map.Entry<ClassReference, AndroidApiLevel> entry : supertypes.entrySet()) {
+ consumer.accept(entry.getKey(), entry.getValue());
+ }
+ }
+
public void registerInterface(ClassReference reference, AndroidApiLevel introApiLevel) {
assert !interfaces.containsKey(reference) : reference + " is already registered";
interfaces.put(reference, introApiLevel);
@@ -61,6 +70,14 @@
interfaces.forEach(consumer);
}
+ /** Visited in insertion order. */
+ public <E extends Throwable> void forEachInterfaceThrowing(
+ ThrowingBiConsumer<ClassReference, AndroidApiLevel, E> consumer) throws E {
+ for (Map.Entry<ClassReference, AndroidApiLevel> entry : interfaces.entrySet()) {
+ consumer.accept(entry.getKey(), entry.getValue());
+ }
+ }
+
public void registerMethod(MethodReference reference, AndroidApiLevel introApiLevel) {
assert !methods.containsKey(reference) : reference + " is already registered";
methods.put(reference, introApiLevel);
diff --git a/src/test/java/com/android/tools/r8/apimodel/AndroidApiHashingDatabaseBuilderGenerator.java b/src/test/java/com/android/tools/r8/apimodel/AndroidApiHashingDatabaseBuilderGenerator.java
deleted file mode 100644
index 55b096b..0000000
--- a/src/test/java/com/android/tools/r8/apimodel/AndroidApiHashingDatabaseBuilderGenerator.java
+++ /dev/null
@@ -1,464 +0,0 @@
-// 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.androidapi.AndroidApiDataAccess.constantPoolHash;
-import static com.android.tools.r8.lightir.ByteUtils.isU2;
-import static com.android.tools.r8.lightir.ByteUtils.setBitAtIndex;
-import static com.android.tools.r8.utils.internal.MapUtils.ignoreKey;
-import static org.junit.Assert.assertEquals;
-
-import com.android.tools.r8.TestBase;
-import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.androidapi.AndroidApiDataAccess;
-import com.android.tools.r8.androidapi.ApiDatabaseEntry;
-import com.android.tools.r8.androidapi.ApiDatabaseEntry.ConstantPoolEntry;
-import com.android.tools.r8.androidapi.GenerateCovariantReturnTypeMethodsTest.CovariantMethodsInJarResult;
-import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
-import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexItemFactory;
-import com.android.tools.r8.graph.DexLibraryClass;
-import com.android.tools.r8.graph.DexMethod;
-import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.references.ClassReference;
-import com.android.tools.r8.utils.AndroidApiLevel;
-import com.android.tools.r8.utils.AndroidApp;
-import com.android.tools.r8.utils.internal.IntBox;
-import com.android.tools.r8.utils.internal.ThrowingBiConsumer;
-import com.android.tools.r8.utils.internal.collections.Pair;
-import com.android.tools.r8.utils.internal.exceptions.Unreachable;
-import com.android.tools.r8.utils.timing.Timing;
-import java.io.ByteArrayOutputStream;
-import java.io.DataOutputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.IdentityHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-public class AndroidApiHashingDatabaseBuilderGenerator extends TestBase {
-
- /**
- * Generate the information needed for looking up api level of references in the android.jar. This
- * method will generate one single database file where the format is as follows (uX is X number of
- * unsigned bytes):
- *
- * <pre>
- * constant_pool_size: u4
- * constant_pool: [constant_pool_size * payload_entry]
- * constant_pool_map: [0..max_hash(ConstantPoolEntry) * payload_entry]
- * api_map: [0..max_hash(ApiDatabaseEntry) * payload_entry]
- * payload raw data.
- *
- * payload_entry: u4:relative_offset_from_payload_start_or_tagged_value + u2:length
- * </pre>
- *
- * For hash_definitions and entries see {@code AndroidApiDataAccess}.
- */
- public static void generate(
- List<ParsedApiClass> apiClasses, Path pathToApiLevels, AndroidApiLevel androidJarApiLevel)
- throws Exception {
- Map<ClassReference, Map<DexMethod, AndroidApiLevel>> methodMap = new HashMap<>();
- Map<ClassReference, Map<FieldTypelessReference, AndroidApiLevel>> fieldMap = new HashMap<>();
- Map<ClassReference, ParsedApiClass> lookupMap = new HashMap<>();
-
- Map<ApiDatabaseEntry, AndroidApiLevel> databaseEntries = new HashMap<>();
-
- Path androidJar = ToolHelper.getAndroidJar(androidJarApiLevel);
- AndroidApp androidApp =
- AndroidApp.builder()
- .addLibraryFile(androidJar)
- .disableAndroidJarHiddenClassExtension()
- .build();
- AppView<AppInfoWithClassHierarchy> appView =
- computeAppViewWithClassHierarchy(androidApp, Timing.empty());
- DexItemFactory factory = appView.dexItemFactory();
-
- CovariantMethodsInJarResult covariantMethodsInJar = CovariantMethodsInJarResult.create();
-
- for (ParsedApiClass apiClass : apiClasses) {
- Map<DexMethod, AndroidApiLevel> methodsForApiClass = new HashMap<>();
- apiClass.forEachMethod(
- (method, apiLevel) -> methodsForApiClass.put(factory.createMethod(method), apiLevel));
- covariantMethodsInJar.visitCovariantMethodsForHolder(
- apiClass.getClassReference(),
- methodReferenceWithApiLevel -> {
- DexMethod method =
- factory.createMethod(methodReferenceWithApiLevel.getMethodReference());
- if (!methodsForApiClass.containsKey(method)) {
- apiClass.registerMethod(
- methodReferenceWithApiLevel.getMethodReference(),
- methodReferenceWithApiLevel.getApiLevel());
- methodsForApiClass.put(method, methodReferenceWithApiLevel.getApiLevel());
- }
- });
- Map<FieldTypelessReference, AndroidApiLevel> fieldsForApiClass = new HashMap<>();
- apiClass.forEachField(fieldsForApiClass::put);
- methodMap.put(apiClass.getClassReference(), methodsForApiClass);
- fieldMap.put(apiClass.getClassReference(), fieldsForApiClass);
- lookupMap.put(apiClass.getClassReference(), apiClass);
-
- databaseEntries.put(
- ApiDatabaseEntry.of(factory.createType(apiClass.getClassReference().getDescriptor())),
- apiClass.getApiLevel());
- }
-
- for (ParsedApiClass apiClass : apiClasses) {
- computeAllReferencesInHierarchy(
- lookupMap,
- factory,
- factory.createType(apiClass.getClassReference().getDescriptor()),
- apiClass,
- AndroidApiLevel.B,
- databaseEntries);
- }
-
- assert ensureAllPublicMethodsAreMapped(
- appView, lookupMap, methodMap, fieldMap, databaseEntries, androidJarApiLevel, androidJar);
-
- try (FileOutputStream fileOutputStream = new FileOutputStream(pathToApiLevels.toFile())) {
- DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream);
- generateDatabase(databaseEntries, dataOutputStream);
- }
- }
-
- private static boolean ensureAllPublicMethodsAreMapped(
- AppView<AppInfoWithClassHierarchy> appView,
- Map<ClassReference, ParsedApiClass> lookupMap,
- Map<ClassReference, Map<DexMethod, AndroidApiLevel>> methodMap,
- Map<ClassReference, Map<FieldTypelessReference, AndroidApiLevel>> fieldMap,
- Map<ApiDatabaseEntry, AndroidApiLevel> databaseEntries,
- AndroidApiLevel apiLevel,
- Path androidJar) {
- Map<DexType, String> missingMemberInformation = new IdentityHashMap<>();
- DexItemFactory factory = appView.dexItemFactory();
- for (DexLibraryClass clazz : appView.app().asDirect().libraryClasses()) {
- ParsedApiClass parsedApiClass = lookupMap.get(clazz.getClassReference());
- if (parsedApiClass == null) {
- if (clazz.isPublic()) {
- missingMemberInformation.put(clazz.getType(), "Could not be found in " + androidJar);
- }
- continue;
- }
- StringBuilder classBuilder = new StringBuilder();
- Map<FieldTypelessReference, AndroidApiLevel> fieldMapForClass =
- fieldMap.get(clazz.getClassReference());
- assert fieldMapForClass != null;
- clazz.forEachClassField(
- field -> {
- if (field.getAccessFlags().isPublic()
- && databaseEntries.get(ApiDatabaseEntry.of(field.getReference())) == null
- && !field.toSourceString().contains("this$0")) {
- classBuilder.append(" ").append(field).append(" is missing\n");
- }
- });
- Map<DexMethod, AndroidApiLevel> methodMapForClass = methodMap.get(clazz.getClassReference());
- assert methodMapForClass != null;
- clazz.forEachClassMethod(
- method -> {
- if (method.getAccessFlags().isPublic()
- && databaseEntries.get(ApiDatabaseEntry.of(method.getReference())) == null
- && !factory.objectMembers.isObjectMember(method.getReference())) {
- classBuilder.append(" ").append(method).append(" is missing\n");
- }
- });
- if (classBuilder.length() > 0) {
- missingMemberInformation.put(clazz.getType(), classBuilder.toString());
- }
- }
-
- Set<DexType> expectedMissingMembers = new HashSet<>();
- // api-versions.xml do not encode all members of StringBuffers and StringBuilders, check that we
- // only have missing definitions for those two classes.
- expectedMissingMembers.add(factory.stringBufferType);
- expectedMissingMembers.add(factory.stringBuilderType);
- // TODO(b/231126636): api-versions.xml has missing definitions for the below classes.
- expectedMissingMembers.add(
- factory.createType("Ljava/util/concurrent/ConcurrentHashMap$KeySetView;"));
- expectedMissingMembers.add(factory.createType("Ljava/time/chrono/ThaiBuddhistDate;"));
- expectedMissingMembers.add(factory.createType("Ljava/time/chrono/HijrahDate;"));
- expectedMissingMembers.add(factory.createType("Ljava/time/chrono/JapaneseDate;"));
- expectedMissingMembers.add(factory.createType("Ljava/time/chrono/MinguoDate;"));
- expectedMissingMembers.add(factory.createType("Landroid/nfc/tech/NfcV;"));
- expectedMissingMembers.add(factory.createType("Landroid/nfc/tech/IsoDep;"));
- expectedMissingMembers.add(factory.createType("Landroid/nfc/tech/MifareUltralight;"));
- expectedMissingMembers.add(factory.createType("Landroid/nfc/tech/MifareClassic;"));
- expectedMissingMembers.add(factory.createType("Landroid/nfc/tech/NdefFormatable;"));
- expectedMissingMembers.add(factory.createType("Landroid/nfc/tech/NfcA;"));
- expectedMissingMembers.add(factory.createType("Landroid/nfc/tech/NfcBarcode;"));
- expectedMissingMembers.add(factory.createType("Landroid/nfc/tech/NfcF;"));
- expectedMissingMembers.add(factory.createType("Landroid/nfc/tech/NfcB;"));
- expectedMissingMembers.add(factory.createType("Landroid/nfc/tech/Ndef;"));
- expectedMissingMembers.add(factory.createType("Landroid/webkit/CookieSyncManager;"));
- if (apiLevel.isLessThan(AndroidApiLevel.BAKLAVA_1)) {
- expectedMissingMembers.add(
- factory.createType("Landroid/adservices/adselection/AdSelectionOutcome;"));
- expectedMissingMembers.add(
- factory.createType("Landroid/adservices/adselection/ReportEventRequest;"));
- expectedMissingMembers.add(
- factory.createType(
- "Landroid/adservices/ondevicepersonalization/FederatedComputeScheduleRequest;"));
- expectedMissingMembers.add(
- factory.createType(
- "Landroid/adservices/ondevicepersonalization/FederatedComputeScheduleResponse;"));
- expectedMissingMembers.add(
- factory.createType(
- "Landroid/adservices/ondevicepersonalization/FederatedComputeScheduler;"));
- expectedMissingMembers.add(
- factory.createType("Landroid/adservices/ondevicepersonalization/InferenceInput;"));
- expectedMissingMembers.add(
- factory.createType(
- "Landroid/adservices/ondevicepersonalization/InferenceInput$Builder;"));
- expectedMissingMembers.add(
- factory.createType("Landroid/adservices/ondevicepersonalization/InferenceInput$Params;"));
- expectedMissingMembers.add(
- factory.createType("Landroid/adservices/ondevicepersonalization/InferenceOutput;"));
- expectedMissingMembers.add(
- factory.createType(
- "Landroid/adservices/ondevicepersonalization/InferenceOutput$Builder;"));
- expectedMissingMembers.add(
- factory.createType(
- "Landroid/adservices/ondevicepersonalization/OnDevicePersonalizationManager;"));
- }
- assertEquals(
- expectedMissingMembers.stream()
- .map(DexType::toDescriptorString)
- .sorted()
- .collect(Collectors.joining("\n"))
- + "\n---\n"
- + missingMemberInformation.keySet().stream()
- .map(DexType::toDescriptorString)
- .sorted()
- .collect(Collectors.joining("\n")),
- expectedMissingMembers,
- missingMemberInformation.keySet());
- return true;
- }
-
- private static class ConstantPool {
-
- private final IntBox intBox = new IntBox(0);
- private final Map<ConstantPoolEntry, Integer> pool = new java.util.LinkedHashMap<>();
-
- public int getOrAdd(ConstantPoolEntry entry) {
- return pool.computeIfAbsent(entry, ignored -> intBox.getAndIncrement());
- }
-
- public void forEach(ThrowingBiConsumer<ConstantPoolEntry, Integer, IOException> consumer)
- throws IOException {
- for (Entry<ConstantPoolEntry, Integer> entry : pool.entrySet()) {
- consumer.accept(entry.getKey(), entry.getValue());
- }
- }
-
- public int size() {
- return pool.size();
- }
- }
-
- private static int setUniqueConstantPoolEntry(int id) {
- return setBitAtIndex(id, 32);
- }
-
- public static void generateDatabase(
- Map<ApiDatabaseEntry, AndroidApiLevel> databaseEntries, DataOutputStream outputStream)
- throws Exception {
- Map<Integer, List<Pair<ApiDatabaseEntry, AndroidApiLevel>>> generationMap = new HashMap<>();
- ConstantPool constantPool = new ConstantPool();
-
- int constantPoolHashMapSize = 1 << AndroidApiDataAccess.entrySizeInBitsForConstantPoolMap();
- int apiHashMapSize = 1 << AndroidApiDataAccess.entrySizeInBitsForApiLevelMap();
-
- for (Entry<ApiDatabaseEntry, AndroidApiLevel> entry : databaseEntries.entrySet()) {
- int newCode = AndroidApiDataAccess.apiLevelHash(entry.getKey());
- assert newCode >= 0 && newCode <= apiHashMapSize;
- generationMap
- .computeIfAbsent(newCode, ignoreKey(ArrayList::new))
- .add(Pair.create(entry.getKey(), entry.getValue()));
- }
-
- Set<String> uniqueHashes = new HashSet<>();
- Map<Integer, Pair<Integer, Integer>> offsetMap = new HashMap<>();
- ByteArrayOutputStream payload = new ByteArrayOutputStream();
-
- // Serialize api map into payload. This will also generate the entire needed constant pool.
- for (Entry<Integer, List<Pair<ApiDatabaseEntry, AndroidApiLevel>>> entry :
- generationMap.entrySet()) {
- int startingOffset = payload.size();
- int length = serializeIntoPayload(entry.getValue(), payload, constantPool, uniqueHashes);
- offsetMap.put(entry.getKey(), Pair.create(startingOffset, length));
- }
-
- // Write constant pool size <u4:size>.
- outputStream.writeInt(constantPool.size());
-
- // Write constant pool consisting of <u4:payload_offset><u2:length>.
- assertEquals(AndroidApiDataAccess.constantPoolOffset(), outputStream.size());
- IntBox lastReadIndex = new IntBox(-1);
- constantPool.forEach(
- (entry, id) -> {
- assert id > lastReadIndex.getAndIncrement();
- outputStream.writeInt(payload.size());
- outputStream.writeShort(entry.getLength());
- entry.writeTo(payload);
- });
-
- // Serialize hash lookup table for constant pool.
- Map<Integer, List<Integer>> constantPoolLookupTable = new HashMap<>();
- constantPool.forEach(
- (entry, id) -> {
- int constantPoolHash = constantPoolHash(entry);
- assert constantPoolHash >= 0 && constantPoolHash <= constantPoolHashMapSize;
- constantPoolLookupTable
- .computeIfAbsent(constantPoolHash, ignoreKey(ArrayList::new))
- .add(id);
- });
-
- int[] constantPoolEntries = new int[constantPoolHashMapSize];
- int[] constantPoolEntryLengths = new int[constantPoolHashMapSize];
- for (Entry<Integer, List<Integer>> entry : constantPoolLookupTable.entrySet()) {
- // Tag if we have a unique value
- if (entry.getValue().size() == 1) {
- int id = entry.getValue().get(0);
- constantPoolEntries[entry.getKey()] = setUniqueConstantPoolEntry(id);
- } else {
- constantPoolEntries[entry.getKey()] = payload.size();
- ByteArrayOutputStream temp = new ByteArrayOutputStream();
- for (Integer id : entry.getValue()) {
- temp.write(intToShortEncodedByteArray(id));
- }
- payload.write(temp.toByteArray());
- constantPoolEntryLengths[entry.getKey()] = temp.size();
- }
- }
- // Write constant pool lookup entries consisting of <u4:payload_offset><u2:length>
- assertEquals(
- AndroidApiDataAccess.constantPoolHashMapOffset(constantPool.size()), outputStream.size());
- for (int i = 0; i < constantPoolEntries.length; i++) {
- outputStream.writeInt(constantPoolEntries[i]);
- outputStream.writeShort(constantPoolEntryLengths[i]);
- }
-
- int[] apiOffsets = new int[apiHashMapSize];
- int[] apiOffsetLengths = new int[apiHashMapSize];
- for (Entry<Integer, Pair<Integer, Integer>> hashIndexAndOffset : offsetMap.entrySet()) {
- assert apiOffsets[hashIndexAndOffset.getKey()] == 0;
- Pair<Integer, Integer> value = hashIndexAndOffset.getValue();
- int offset = value.getFirst();
- int length = value.getSecond();
- apiOffsets[hashIndexAndOffset.getKey()] = offset;
- apiOffsetLengths[hashIndexAndOffset.getKey()] = length;
- }
-
- // Write api lookup entries consisting of <u4:payload_offset><u2:length>
- assertEquals(
- AndroidApiDataAccess.apiLevelHashMapOffset(constantPool.size()), outputStream.size());
- for (int i = 0; i < apiOffsets.length; i++) {
- outputStream.writeInt(apiOffsets[i]);
- outputStream.writeShort(apiOffsetLengths[i]);
- }
-
- // Write the payload.
- outputStream.write(payload.toByteArray());
- }
-
- /** This will serialize a collection of DexReferences and apis into a byte stream. */
- private static int serializeIntoPayload(
- List<Pair<ApiDatabaseEntry, AndroidApiLevel>> pairs,
- ByteArrayOutputStream payload,
- ConstantPool constantPool,
- Set<String> seen)
- throws IOException {
- ByteArrayOutputStream temp = new ByteArrayOutputStream();
- for (Pair<ApiDatabaseEntry, AndroidApiLevel> pair : pairs) {
- byte[] uniqueDescriptorForReference =
- pair.getFirst().getUniqueDescriptor(constantPool::getOrAdd);
- assert uniqueDescriptorForReference != ApiDatabaseEntry.getNonExistingDescriptor();
- if (!seen.add(Arrays.toString(uniqueDescriptorForReference))) {
- throw new Unreachable("Hash is not unique");
- }
- temp.write(intToShortEncodedByteArray(uniqueDescriptorForReference.length));
- temp.write(uniqueDescriptorForReference);
- assert pair.getSecond() != AndroidApiLevel.MAIN
- && pair.getSecond() != AndroidApiLevel.EXTENSION;
- temp.write(pair.getSecond().serializeAsByte());
- }
- byte[] tempArray = temp.toByteArray();
- payload.write(tempArray);
- return tempArray.length;
- }
-
- public static byte[] intToShortEncodedByteArray(int value) {
- assert isU2(value);
- byte[] bytes = new byte[2];
- bytes[0] = (byte) (value >> 8);
- bytes[1] = (byte) value;
- return bytes;
- }
-
- private static void computeAllReferencesInHierarchy(
- Map<ClassReference, ParsedApiClass> lookupMap,
- DexItemFactory factory,
- DexType holder,
- ParsedApiClass apiClass,
- AndroidApiLevel linkLevel,
- Map<ApiDatabaseEntry, AndroidApiLevel> databaseEntries) {
- if (!apiClass.getClassReference().getDescriptor().equals(factory.objectDescriptor.toString())) {
- apiClass.forEachMethod(
- (methodReference, apiLevel) ->
- addIfNewOrApiLevelIsLower(
- linkLevel,
- databaseEntries,
- apiLevel,
- ApiDatabaseEntry.of(
- factory.createMethod(methodReference).withHolder(holder, factory))));
- apiClass.forEachField(
- (fieldReference, apiLevel) ->
- addIfNewOrApiLevelIsLower(
- linkLevel,
- databaseEntries,
- apiLevel,
- ApiDatabaseEntry.of(fieldReference).withHolder(holder)));
- apiClass.forEachSupertype(
- (superType, apiLevel) ->
- computeAllReferencesInHierarchy(
- lookupMap,
- factory,
- holder,
- lookupMap.get(superType),
- linkLevel.max(apiLevel),
- databaseEntries));
- apiClass.forEachInterface(
- (interfaceReference, apiLevel) ->
- computeAllReferencesInHierarchy(
- lookupMap,
- factory,
- holder,
- lookupMap.get(interfaceReference),
- linkLevel.max(apiLevel),
- databaseEntries));
- }
- }
-
- private static void addIfNewOrApiLevelIsLower(
- AndroidApiLevel linkLevel,
- Map<ApiDatabaseEntry, AndroidApiLevel> additionMap,
- AndroidApiLevel apiLevel,
- ApiDatabaseEntry member) {
- AndroidApiLevel currentApiLevel = apiLevel.max(linkLevel);
- AndroidApiLevel existingApiLevel = additionMap.get(member);
- if (existingApiLevel == null || currentApiLevel.isLessThanOrEqualTo(existingApiLevel)) {
- additionMap.put(member, currentApiLevel);
- }
- }
-}
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 ce2c65f..b7b5dc8 100644
--- a/src/test/java/com/android/tools/r8/apimodel/AndroidApiHashingDatabaseBuilderGeneratorTest.java
+++ b/src/test/java/com/android/tools/r8/apimodel/AndroidApiHashingDatabaseBuilderGeneratorTest.java
@@ -22,7 +22,9 @@
import com.android.tools.r8.androidapi.AndroidApiLevelCompute;
import com.android.tools.r8.androidapi.AndroidApiLevelCompute.DefaultAndroidApiLevelCompute;
import com.android.tools.r8.androidapi.AndroidApiLevelHashingDatabaseImpl;
+import com.android.tools.r8.androidapi.ApiDatabaseEntry;
import com.android.tools.r8.androidapi.ComputedApiLevel;
+import com.android.tools.r8.androidapi.GenerateCovariantReturnTypeMethodsTest.CovariantMethodsInJarResult;
import com.android.tools.r8.androidapi.SunMiscUnsafeApiTest;
import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
import com.android.tools.r8.graph.AppView;
@@ -55,6 +57,7 @@
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -113,10 +116,166 @@
TemporaryFolder temp = new TemporaryFolder();
temp.create();
Path apiLevels = temp.newFile("new_api_levels.ser").toPath();
- AndroidApiHashingDatabaseBuilderGenerator.generate(apiClasses, apiLevels, androidJarApiLevel);
+ DexItemFactory factory = new DexItemFactory();
+ mergeCovariantMethods(apiClasses, factory);
+
+ Map<ApiDatabaseEntry, AndroidApiLevel> databaseEntries =
+ AndroidApiHashingDatabaseBuilderGenerator.generateEntries(apiClasses);
+
+ Path androidJar = ToolHelper.getAndroidJar(androidJarApiLevel);
+ AndroidApp androidApp =
+ AndroidApp.builder()
+ .addLibraryFile(androidJar)
+ .disableAndroidJarHiddenClassExtension()
+ .build();
+ AppView<AppInfoWithClassHierarchy> appView =
+ computeAppViewWithClassHierarchy(androidApp, Timing.empty());
+
+ ensureAllPublicMethodsAreMapped(
+ appView, apiClasses, databaseEntries, androidJarApiLevel, androidJar);
+
+ AndroidApiHashingDatabaseBuilderGenerator.writeEntries(databaseEntries, apiLevels);
+
return new GenerateDatabaseResourceFilesResult(apiLevels);
}
+ private static void ensureAllPublicMethodsAreMapped(
+ AppView<AppInfoWithClassHierarchy> appView,
+ List<ParsedApiClass> apiClasses,
+ Map<ApiDatabaseEntry, AndroidApiLevel> databaseEntries,
+ AndroidApiLevel apiLevel,
+ Path androidJar) {
+ Map<ClassReference, ParsedApiClass> lookupMap = new HashMap<>();
+ Map<ClassReference, Map<DexMethod, AndroidApiLevel>> methodMap = new HashMap<>();
+ Map<ClassReference, Map<FieldTypelessReference, AndroidApiLevel>> fieldMap = new HashMap<>();
+ DexItemFactory factory = appView.dexItemFactory();
+
+ for (ParsedApiClass apiClass : apiClasses) {
+ lookupMap.put(apiClass.getClassReference(), apiClass);
+ Map<DexMethod, AndroidApiLevel> methodsForApiClass = new HashMap<>();
+ apiClass.forEachMethod(
+ (method, lvl) -> methodsForApiClass.put(factory.createMethod(method), lvl));
+ methodMap.put(apiClass.getClassReference(), methodsForApiClass);
+
+ Map<FieldTypelessReference, AndroidApiLevel> fieldsForApiClass = new HashMap<>();
+ apiClass.forEachField(fieldsForApiClass::put);
+ fieldMap.put(apiClass.getClassReference(), fieldsForApiClass);
+ }
+
+ Map<DexType, String> missingMemberInformation = new IdentityHashMap<>();
+ for (DexLibraryClass clazz : appView.app().asDirect().libraryClasses()) {
+ ParsedApiClass parsedApiClass = lookupMap.get(clazz.getClassReference());
+ if (parsedApiClass == null) {
+ if (clazz.isPublic()) {
+ missingMemberInformation.put(clazz.getType(), "Could not be found in " + androidJar);
+ }
+ continue;
+ }
+ StringBuilder classBuilder = new StringBuilder();
+ Map<FieldTypelessReference, AndroidApiLevel> fieldMapForClass =
+ fieldMap.get(clazz.getClassReference());
+ assert fieldMapForClass != null;
+ clazz.forEachClassField(
+ field -> {
+ if (field.getAccessFlags().isPublic()
+ && databaseEntries.get(ApiDatabaseEntry.of(field.getReference())) == null
+ && !field.toSourceString().contains("this$0")) {
+ classBuilder.append(" ").append(field).append(" is missing\n");
+ }
+ });
+ Map<DexMethod, AndroidApiLevel> methodMapForClass = methodMap.get(clazz.getClassReference());
+ assert methodMapForClass != null;
+ clazz.forEachClassMethod(
+ method -> {
+ if (method.getAccessFlags().isPublic()
+ && databaseEntries.get(ApiDatabaseEntry.of(method.getReference())) == null
+ && !factory.objectMembers.isObjectMember(method.getReference())) {
+ classBuilder.append(" ").append(method).append(" is missing\n");
+ }
+ });
+ if (classBuilder.length() > 0) {
+ missingMemberInformation.put(clazz.getType(), classBuilder.toString());
+ }
+ }
+
+ Set<DexType> expectedMissingMembers = new HashSet<>();
+ // api-versions.xml do not encode all members of StringBuffers and StringBuilders, check that we
+ // only have missing definitions for those two classes.
+ expectedMissingMembers.add(factory.stringBufferType);
+ expectedMissingMembers.add(factory.stringBuilderType);
+ // TODO(b/231126636): api-versions.xml has missing definitions for the below classes.
+ expectedMissingMembers.add(
+ factory.createType("Ljava/util/concurrent/ConcurrentHashMap$KeySetView;"));
+ expectedMissingMembers.add(factory.createType("Ljava/time/chrono/ThaiBuddhistDate;"));
+ expectedMissingMembers.add(factory.createType("Ljava/time/chrono/HijrahDate;"));
+ expectedMissingMembers.add(factory.createType("Ljava/time/chrono/JapaneseDate;"));
+ expectedMissingMembers.add(factory.createType("Ljava/time/chrono/MinguoDate;"));
+ expectedMissingMembers.add(factory.createType("Landroid/nfc/tech/NfcV;"));
+ expectedMissingMembers.add(factory.createType("Landroid/nfc/tech/IsoDep;"));
+ expectedMissingMembers.add(factory.createType("Landroid/nfc/tech/MifareUltralight;"));
+ expectedMissingMembers.add(factory.createType("Landroid/nfc/tech/MifareClassic;"));
+ expectedMissingMembers.add(factory.createType("Landroid/nfc/tech/NdefFormatable;"));
+ expectedMissingMembers.add(factory.createType("Landroid/nfc/tech/NfcA;"));
+ expectedMissingMembers.add(factory.createType("Landroid/nfc/tech/NfcBarcode;"));
+ expectedMissingMembers.add(factory.createType("Landroid/nfc/tech/NfcF;"));
+ expectedMissingMembers.add(factory.createType("Landroid/nfc/tech/NfcB;"));
+ expectedMissingMembers.add(factory.createType("Landroid/nfc/tech/Ndef;"));
+ expectedMissingMembers.add(factory.createType("Landroid/webkit/CookieSyncManager;"));
+ if (apiLevel.isLessThan(AndroidApiLevel.BAKLAVA_1)) {
+ expectedMissingMembers.add(
+ factory.createType("Landroid/adservices/adselection/AdSelectionOutcome;"));
+ expectedMissingMembers.add(
+ factory.createType("Landroid/adservices/adselection/ReportEventRequest;"));
+ expectedMissingMembers.add(
+ factory.createType(
+ "Landroid/adservices/ondevicepersonalization/FederatedComputeScheduleRequest;"));
+ expectedMissingMembers.add(
+ factory.createType(
+ "Landroid/adservices/ondevicepersonalization/FederatedComputeScheduleResponse;"));
+ expectedMissingMembers.add(
+ factory.createType(
+ "Landroid/adservices/ondevicepersonalization/FederatedComputeScheduler;"));
+ expectedMissingMembers.add(
+ factory.createType("Landroid/adservices/ondevicepersonalization/InferenceInput;"));
+ expectedMissingMembers.add(
+ factory.createType(
+ "Landroid/adservices/ondevicepersonalization/InferenceInput$Builder;"));
+ expectedMissingMembers.add(
+ factory.createType("Landroid/adservices/ondevicepersonalization/InferenceInput$Params;"));
+ expectedMissingMembers.add(
+ factory.createType("Landroid/adservices/ondevicepersonalization/InferenceOutput;"));
+ expectedMissingMembers.add(
+ factory.createType(
+ "Landroid/adservices/ondevicepersonalization/InferenceOutput$Builder;"));
+ expectedMissingMembers.add(
+ factory.createType(
+ "Landroid/adservices/ondevicepersonalization/OnDevicePersonalizationManager;"));
+ }
+ assertThat(missingMemberInformation.keySet(), matchesItemsOneToOne(expectedMissingMembers));
+ }
+
+ private static void mergeCovariantMethods(List<ParsedApiClass> apiClasses, DexItemFactory factory)
+ throws Exception {
+ CovariantMethodsInJarResult covariantMethodsInJar = CovariantMethodsInJarResult.create();
+ for (ParsedApiClass apiClass : apiClasses) {
+ Map<DexMethod, AndroidApiLevel> methodsForApiClass = new HashMap<>();
+ apiClass.forEachMethod(
+ (method, apiLevel) -> methodsForApiClass.put(factory.createMethod(method), apiLevel));
+ covariantMethodsInJar.visitCovariantMethodsForHolder(
+ apiClass.getClassReference(),
+ methodReferenceWithApiLevel -> {
+ DexMethod method =
+ factory.createMethod(methodReferenceWithApiLevel.getMethodReference());
+ if (!methodsForApiClass.containsKey(method)) {
+ apiClass.registerMethod(
+ methodReferenceWithApiLevel.getMethodReference(),
+ methodReferenceWithApiLevel.getApiLevel());
+ methodsForApiClass.put(method, methodReferenceWithApiLevel.getApiLevel());
+ }
+ });
+ }
+ }
+
@Test
public void testCanParseApiVersionsXml() throws Exception {
// This tests makes a rudimentary check on the number of classes, fields and methods in
diff --git a/third_party/api_database/api_database.tar.gz.sha1 b/third_party/api_database/api_database.tar.gz.sha1
index f5a7dcf..839dea4 100644
--- a/third_party/api_database/api_database.tar.gz.sha1
+++ b/third_party/api_database/api_database.tar.gz.sha1
@@ -1 +1 @@
-83f980bfe52868bf67129526ea1c626ba5f37fec
\ No newline at end of file
+ebc62b3efc8ffa79035399941c1c3d8bf995b5ec
\ No newline at end of file