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