Merge commit 'c3f20e7a6db394955ae6359bb4c7f11de74453be' into dev-release
diff --git a/build.gradle b/build.gradle
index 5220ac2..8e20e6d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -428,13 +428,7 @@
         "gmail/gmail_android_170604.16",
         "gmail/gmail_android_180826.15",
         "gmscore/gmscore_v10",
-        "gmscore/gmscore_v9",
         "gmscore/latest",
-        "gmscore/v4",
-        "gmscore/v5",
-        "gmscore/v6",
-        "gmscore/v7",
-        "gmscore/v8",
         "nest/nest_20180926_7c6cfb",
         "proguard/proguard_internal_159423826",
         "proguardsettings",
diff --git a/src/main/java/com/android/tools/r8/L8Command.java b/src/main/java/com/android/tools/r8/L8Command.java
index ec9955d..6fea9a2 100644
--- a/src/main/java/com/android/tools/r8/L8Command.java
+++ b/src/main/java/com/android/tools/r8/L8Command.java
@@ -160,6 +160,7 @@
     assert !internal.minimalMainDex;
     internal.minApiLevel = getMinApiLevel();
     assert !internal.intermediate;
+    internal.intermediate = true;
     assert internal.readCompileTimeAnnotations;
     internal.programConsumer = getProgramConsumer();
     assert internal.programConsumer instanceof ClassFileConsumer;
diff --git a/src/main/java/com/android/tools/r8/graph/AppInfo.java b/src/main/java/com/android/tools/r8/graph/AppInfo.java
index ab55c16..df45e6b 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfo.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfo.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.graph;
 
+import com.android.tools.r8.FeatureSplit;
 import com.android.tools.r8.graph.FieldResolutionResult.SuccessfulFieldResolutionResult;
 import com.android.tools.r8.ir.desugar.itf.InterfaceMethodRewriter;
 import com.android.tools.r8.origin.Origin;
@@ -130,13 +131,15 @@
   public void addSynthesizedClass(DexProgramClass clazz, ProgramDefinition context) {
     assert checkIfObsolete();
     assert context != null;
-    syntheticItems.addLegacySyntheticClass(clazz, context);
+    syntheticItems.addLegacySyntheticClass(clazz, context, FeatureSplit.BASE);
   }
 
-  public void addSynthesizedClass(DexProgramClass clazz, Iterable<DexProgramClass> contexts) {
+  public void addSynthesizedClassToBase(DexProgramClass clazz, Iterable<DexProgramClass> contexts) {
     assert checkIfObsolete();
     assert !IterableUtils.isEmpty(contexts);
-    contexts.forEach(context -> addSynthesizedClass(clazz, context));
+    SyntheticItems syntheticItems = getSyntheticItems();
+    contexts.forEach(
+        context -> syntheticItems.addLegacySyntheticClass(clazz, context, FeatureSplit.BASE));
   }
 
   public List<DexProgramClass> classes() {
diff --git a/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java b/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java
index c2c02ad..9b55af9 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java
@@ -7,6 +7,7 @@
 import static com.android.tools.r8.utils.TraversalContinuation.BREAK;
 import static com.android.tools.r8.utils.TraversalContinuation.CONTINUE;
 
+import com.android.tools.r8.FeatureSplit;
 import com.android.tools.r8.features.ClassToFeatureSplitMap;
 import com.android.tools.r8.graph.FieldResolutionResult.SuccessfulFieldResolutionResult;
 import com.android.tools.r8.graph.ResolutionResult.ArrayCloneMethodResult;
@@ -156,6 +157,15 @@
     return this;
   }
 
+  @Override
+  public void addSynthesizedClass(DexProgramClass clazz, ProgramDefinition context) {
+    assert checkIfObsolete();
+    assert context != null;
+    FeatureSplit featureSplit =
+        classToFeatureSplitMap.getFeatureSplit(context, getSyntheticItems());
+    getSyntheticItems().addLegacySyntheticClass(clazz, context, featureSplit);
+  }
+
   /**
    * Primitive traversal over all supertypes of a given type.
    *
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceMethodRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceMethodRewriter.java
index ea1d102..6a878f0 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceMethodRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceMethodRewriter.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.ir.desugar.itf;
 
 import static com.android.tools.r8.ir.code.Invoke.Type.DIRECT;
+import static com.android.tools.r8.ir.code.Invoke.Type.INTERFACE;
 import static com.android.tools.r8.ir.code.Invoke.Type.STATIC;
 import static com.android.tools.r8.ir.code.Invoke.Type.SUPER;
 import static com.android.tools.r8.ir.code.Invoke.Type.VIRTUAL;
@@ -231,8 +232,12 @@
       if (clazz != null && clazz.isInterface()) {
         return true;
       }
+      return emulatedMethods.contains(method.getName());
     }
-    return emulatedMethods.contains(method.getName());
+    if (invokeType == VIRTUAL || invokeType == INTERFACE) {
+      return defaultMethodForEmulatedDispatchOrNull(method, invokeType) != null;
+    }
+    return true;
   }
 
   DexType getEmulatedInterface(DexType itf) {
@@ -621,27 +626,41 @@
     }
   }
 
-  private void rewriteInvokeInterfaceOrInvokeVirtual(
-      InvokeMethodWithReceiver invoke, InstructionListIterator instructions) {
-    DexMethod invokedMethod = invoke.getInvokedMethod();
-    DexType emulatedItf = maximallySpecificEmulatedInterfaceOrNull(invokedMethod);
+  private DexClassAndMethod defaultMethodForEmulatedDispatchOrNull(
+      DexMethod method, Type invokeType) {
+    assert invokeType == VIRTUAL || invokeType == INTERFACE;
+    boolean interfaceBit = invokeType.isInterface();
+    DexType emulatedItf = maximallySpecificEmulatedInterfaceOrNull(method);
     if (emulatedItf == null) {
-      return;
+      return null;
     }
-
     // The call potentially ends up in a library class, in which case we need to rewrite, since the
     // code may be in the desugared library.
     SingleResolutionResult resolution =
-        appView
-            .appInfoForDesugaring()
-            .resolveMethod(invokedMethod, invoke.getInterfaceBit())
-            .asSingleResolution();
+        appView.appInfoForDesugaring().resolveMethod(method, interfaceBit).asSingleResolution();
     if (resolution != null
         && (resolution.getResolvedHolder().isLibraryClass()
             || appView.options().isDesugaredLibraryCompilation())) {
-      assert needsRewriting(invokedMethod, VIRTUAL);
-      rewriteCurrentInstructionToEmulatedInterfaceCall(
-          emulatedItf, invokedMethod, invoke, instructions);
+      DexClassAndMethod defaultMethod =
+          appView.definitionFor(emulatedItf).lookupClassMethod(method);
+      if (defaultMethod != null && !dontRewrite(defaultMethod)) {
+        assert !defaultMethod.getAccessFlags().isAbstract();
+        return defaultMethod;
+      }
+    }
+    return null;
+  }
+
+  private void rewriteInvokeInterfaceOrInvokeVirtual(
+      InvokeMethodWithReceiver invoke, InstructionListIterator instructions) {
+    DexClassAndMethod defaultMethod =
+        defaultMethodForEmulatedDispatchOrNull(invoke.getInvokedMethod(), invoke.getType());
+    if (defaultMethod != null) {
+      instructions.replaceCurrentInstruction(
+          new InvokeStatic(
+              emulateInterfaceLibraryMethod(defaultMethod, factory),
+              invoke.outValue(),
+              invoke.arguments()));
     }
   }
 
@@ -733,23 +752,6 @@
     return null;
   }
 
-  private void rewriteCurrentInstructionToEmulatedInterfaceCall(
-      DexType emulatedItf,
-      DexMethod invokedMethod,
-      InvokeMethod invokeMethod,
-      InstructionListIterator instructions) {
-    DexClassAndMethod defaultMethod =
-        appView.definitionFor(emulatedItf).lookupClassMethod(invokedMethod);
-    if (defaultMethod != null && !dontRewrite(defaultMethod)) {
-      assert !defaultMethod.getAccessFlags().isAbstract();
-      instructions.replaceCurrentInstruction(
-          new InvokeStatic(
-              emulateInterfaceLibraryMethod(defaultMethod, factory),
-              invokeMethod.outValue(),
-              invokeMethod.arguments()));
-    }
-  }
-
   private boolean isNonDesugaredLibraryClass(DexClass clazz) {
     return clazz.isLibraryClass() && !isInDesugaredLibrary(clazz);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceProcessor.java b/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceProcessor.java
index e8e1cdd..e847977 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceProcessor.java
@@ -432,6 +432,10 @@
       }
     }
     if (method.accessFlags.isBridge()) {
+      if (appView.options().cfToCfDesugar) {
+        // TODO(b/187176895): Find the compilation causing this to not be removed.
+        return false;
+      }
       Deque<Pair<DexClass, DexType>> worklist = new ArrayDeque<>();
       Set<DexType> seenBefore = new HashSet<>();
       addSuperTypes(iface, worklist);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/UnboxedEnumMemberRelocator.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/UnboxedEnumMemberRelocator.java
index c29dd86..54c416a 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/UnboxedEnumMemberRelocator.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/UnboxedEnumMemberRelocator.java
@@ -179,7 +179,7 @@
               appView.dexItemFactory().getSkipNameValidationForTesting(),
               DexProgramClass::checksumFromType);
       appBuilder.addSynthesizedClass(syntheticClass);
-      appView.appInfo().addSynthesizedClass(syntheticClass, contexts);
+      appView.appInfo().addSynthesizedClassToBase(syntheticClass, contexts);
       return syntheticClass;
     }
 
diff --git a/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java b/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
index 9da3a40..5f23e34 100644
--- a/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
@@ -22,6 +22,7 @@
 import com.android.tools.r8.graph.DexEncodedAnnotation;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
@@ -42,17 +43,19 @@
 import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.synthesis.SyntheticNaming;
 import com.android.tools.r8.utils.AsmUtils;
+import com.android.tools.r8.utils.ComparatorUtils;
 import com.android.tools.r8.utils.ExceptionUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.PredicateUtils;
 import com.android.tools.r8.utils.structural.Ordered;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableMap.Builder;
-import com.google.common.collect.Sets;
 import java.io.PrintWriter;
 import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
 import java.util.Optional;
-import java.util.SortedSet;
 import java.util.function.Predicate;
 import org.objectweb.asm.AnnotationVisitor;
 import org.objectweb.asm.ClassReader;
@@ -236,12 +239,10 @@
       writeField(field, writer);
     }
     if (options.desugarSpecificOptions().sortMethodsOnCfOutput) {
-      SortedSet<ProgramMethod> programMethodSortedSet =
-          Sets.newTreeSet(
-              (a, b) ->
-                  a.getDefinition().getReference().compareTo(b.getDefinition().getReference()));
-      clazz.forEachProgramMethod(programMethodSortedSet::add);
-      programMethodSortedSet.forEach(
+      List<ProgramMethod> programMethodSorted = new ArrayList<>();
+      clazz.forEachProgramMethod(programMethodSorted::add);
+      programMethodSorted.sort(this::compareMethodsThroughLens);
+      programMethodSorted.forEach(
           method -> writeMethod(method, version, rewriter, writer, defaults));
     } else {
       clazz.forEachProgramMethod(
@@ -263,6 +264,26 @@
         options.reporter, handler -> consumer.accept(ByteDataView.of(result), desc, handler));
   }
 
+  private int compareTypesThroughLens(DexType a, DexType b) {
+    return namingLens.lookupDescriptor(a).compareTo(namingLens.lookupDescriptor(b));
+  }
+
+  private DexString returnTypeThroughLens(DexMethod method) {
+    return namingLens.lookupDescriptor(method.getReturnType());
+  }
+
+  private int compareMethodsThroughLens(ProgramMethod a, ProgramMethod b) {
+    // When writing class files, methods are only compared within the same class.
+    assert a.getHolder().equals(b.getHolder());
+    return Comparator.comparing(this::returnTypeThroughLens)
+        .thenComparing(DexMethod::getName)
+        // .thenComparingInt(m -> m.getProto().getArity()) // Done in arrayComp below.
+        .thenComparing(
+            m -> m.getProto().parameters.values,
+            ComparatorUtils.arrayComparator(this::compareTypesThroughLens))
+        .compare(a.getReference(), b.getReference());
+  }
+
   private CfVersion getClassFileVersion(DexEncodedMethod method) {
     if (!method.hasClassFileVersion()) {
       // In this case bridges have been introduced for the Cf back-end,
diff --git a/src/main/java/com/android/tools/r8/retrace/internal/RetraceMethodResultImpl.java b/src/main/java/com/android/tools/r8/retrace/internal/RetraceMethodResultImpl.java
index 7e66ec1..735a163 100644
--- a/src/main/java/com/android/tools/r8/retrace/internal/RetraceMethodResultImpl.java
+++ b/src/main/java/com/android/tools/r8/retrace/internal/RetraceMethodResultImpl.java
@@ -152,7 +152,7 @@
 
     @Override
     public RetracedMethodReference getRetracedMethod() {
-      return null;
+      return methodReference;
     }
 
     @Override
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index d377362..f6d9e6e 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -156,6 +156,7 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.function.BiConsumer;
@@ -3065,7 +3066,7 @@
 
     private final ProcessorContext processorContext;
     private Map<DexMethod, MethodProcessingContext> methodProcessingContexts =
-        new IdentityHashMap<>();
+        new ConcurrentHashMap<>();
 
     List<ProgramMethod> desugaredMethods = new LinkedList<>();
 
diff --git a/src/main/java/com/android/tools/r8/synthesis/CommittedItems.java b/src/main/java/com/android/tools/r8/synthesis/CommittedItems.java
index adc1298..f71c94e 100644
--- a/src/main/java/com/android/tools/r8/synthesis/CommittedItems.java
+++ b/src/main/java/com/android/tools/r8/synthesis/CommittedItems.java
@@ -38,7 +38,7 @@
     this.application = application;
     this.committed = committed;
     this.committedProgramTypes = committedProgramTypes;
-    committed.verifyTypesAreInApp(application);
+    assert committed.verifyTypesAreInApp(application);
   }
 
   // Conversion to a mutable synthetic items collection. Should only be used in AppInfo creation.
diff --git a/src/main/java/com/android/tools/r8/synthesis/CommittedSyntheticsCollection.java b/src/main/java/com/android/tools/r8/synthesis/CommittedSyntheticsCollection.java
index f8c56fa..cd80b53 100644
--- a/src/main/java/com/android/tools/r8/synthesis/CommittedSyntheticsCollection.java
+++ b/src/main/java/com/android/tools/r8/synthesis/CommittedSyntheticsCollection.java
@@ -298,23 +298,18 @@
   }
 
   CommittedSyntheticsCollection rewriteWithLens(NonIdentityGraphLens lens) {
+    ImmutableSet.Builder<DexType> syntheticInputsBuilder = ImmutableSet.builder();
     return new CommittedSyntheticsCollection(
-        rewriteItems(legacyTypes, lens),
-        rewriteItems(nonLegacyMethods, lens),
-        rewriteItems(nonLegacyClasses, lens),
-        rewriteItems(syntheticInputs, lens));
-  }
-
-  private static ImmutableSet<DexType> rewriteItems(Set<DexType> items, NonIdentityGraphLens lens) {
-    ImmutableSet.Builder<DexType> rewrittenItems = ImmutableSet.builder();
-    for (DexType item : items) {
-      rewrittenItems.add(lens.lookupType(item));
-    }
-    return rewrittenItems.build();
+        rewriteItems(legacyTypes, lens, syntheticInputsBuilder),
+        rewriteItems(nonLegacyMethods, lens, syntheticInputsBuilder),
+        rewriteItems(nonLegacyClasses, lens, syntheticInputsBuilder),
+        syntheticInputsBuilder.build());
   }
 
   private <R extends Rewritable<R>> ImmutableMap<DexType, List<R>> rewriteItems(
-      Map<DexType, List<R>> items, NonIdentityGraphLens lens) {
+      Map<DexType, List<R>> items,
+      NonIdentityGraphLens lens,
+      ImmutableSet.Builder<DexType> syntheticInputsBuilder) {
     Map<DexType, List<R>> rewrittenItems = new IdentityHashMap<>();
     for (R reference : IterableUtils.flatten(items.values())) {
       R rewritten = reference.rewrite(lens);
@@ -322,6 +317,9 @@
         rewrittenItems
             .computeIfAbsent(rewritten.getHolder(), ignore -> new ArrayList<>())
             .add(rewritten);
+        if (syntheticInputs.contains(reference.getHolder())) {
+          syntheticInputsBuilder.add(rewritten.getHolder());
+        }
       }
     }
     return ImmutableMap.copyOf(rewrittenItems);
diff --git a/src/main/java/com/android/tools/r8/synthesis/LegacySyntheticDefinition.java b/src/main/java/com/android/tools/r8/synthesis/LegacySyntheticDefinition.java
index 47a9556..3e50bf5 100644
--- a/src/main/java/com/android/tools/r8/synthesis/LegacySyntheticDefinition.java
+++ b/src/main/java/com/android/tools/r8/synthesis/LegacySyntheticDefinition.java
@@ -3,25 +3,28 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.synthesis;
 
+import com.android.tools.r8.FeatureSplit;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramDefinition;
+import com.android.tools.r8.utils.IterableUtils;
 import com.google.common.collect.ImmutableSet;
+import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 
 public class LegacySyntheticDefinition {
   private final DexProgramClass clazz;
-  private final Map<DexType, DexType> contexts = new ConcurrentHashMap<>();
+  private final Map<DexType, FeatureSplit> contexts = new ConcurrentHashMap<>();
 
   public LegacySyntheticDefinition(DexProgramClass clazz) {
     this.clazz = clazz;
   }
 
-  public void addContext(ProgramDefinition clazz) {
+  public void addContext(ProgramDefinition clazz, FeatureSplit featureSplit) {
     DexType type = clazz.getContextType();
-    contexts.put(type, type);
+    contexts.put(type, featureSplit);
   }
 
   public Set<DexType> getContexts() {
@@ -29,7 +32,22 @@
   }
 
   public LegacySyntheticReference toReference() {
-    return new LegacySyntheticReference(clazz.getType(), ImmutableSet.copyOf(contexts.keySet()));
+    return new LegacySyntheticReference(
+        clazz.getType(), ImmutableSet.copyOf(contexts.keySet()), getFeatureSplit());
+  }
+
+  public FeatureSplit getFeatureSplit() {
+    assert verifyConsistentFeatures();
+    if (contexts.isEmpty()) {
+      return FeatureSplit.BASE;
+    }
+    return IterableUtils.first(contexts.values());
+  }
+
+  private boolean verifyConsistentFeatures() {
+    HashSet<FeatureSplit> features = new HashSet<>(contexts.values());
+    assert features.size() < 2;
+    return true;
   }
 
   public DexProgramClass getDefinition() {
diff --git a/src/main/java/com/android/tools/r8/synthesis/LegacySyntheticReference.java b/src/main/java/com/android/tools/r8/synthesis/LegacySyntheticReference.java
index 06d2e2c..715e790 100644
--- a/src/main/java/com/android/tools/r8/synthesis/LegacySyntheticReference.java
+++ b/src/main/java/com/android/tools/r8/synthesis/LegacySyntheticReference.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.synthesis;
 
+import com.android.tools.r8.FeatureSplit;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.GraphLens.NonIdentityGraphLens;
 import java.util.Set;
@@ -10,10 +11,12 @@
 public class LegacySyntheticReference implements Rewritable<LegacySyntheticReference> {
   private final DexType type;
   private final Set<DexType> contexts;
+  private final FeatureSplit featureSplit;
 
-  public LegacySyntheticReference(DexType type, Set<DexType> contexts) {
+  public LegacySyntheticReference(DexType type, Set<DexType> contexts, FeatureSplit featureSplit) {
     this.type = type;
     this.contexts = contexts;
+    this.featureSplit = featureSplit;
   }
 
   @Override
@@ -25,6 +28,10 @@
     return contexts;
   }
 
+  public FeatureSplit getFeatureSplit() {
+    return featureSplit;
+  }
+
   @Override
   public LegacySyntheticReference rewrite(NonIdentityGraphLens lens) {
     DexType rewrittenType = lens.lookupType(type);
@@ -32,6 +39,6 @@
     if (type == rewrittenType && contexts.equals(rewrittenContexts)) {
       return this;
     }
-    return new LegacySyntheticReference(rewrittenType, rewrittenContexts);
+    return new LegacySyntheticReference(rewrittenType, rewrittenContexts, featureSplit);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticDefinition.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticDefinition.java
index 3c91d68..ba68e81 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticDefinition.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticDefinition.java
@@ -92,17 +92,15 @@
       D other,
       boolean includeContext,
       GraphLens graphLens,
-      ClassToFeatureSplitMap classToFeatureSplitMap,
-      SyntheticItems syntheticItems) {
-    return compareTo(other, includeContext, graphLens, classToFeatureSplitMap, syntheticItems) == 0;
+      ClassToFeatureSplitMap classToFeatureSplitMap) {
+    return compareTo(other, includeContext, graphLens, classToFeatureSplitMap) == 0;
   }
 
   int compareTo(
       D other,
       boolean includeContext,
       GraphLens graphLens,
-      ClassToFeatureSplitMap classToFeatureSplitMap,
-      SyntheticItems syntheticItems) {
+      ClassToFeatureSplitMap classToFeatureSplitMap) {
     DexType thisType = getHolder().getType();
     DexType otherType = other.getHolder().getType();
     if (getKind().isFixedSuffixSynthetic) {
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
index 46bb432..ac15138 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedMember;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItemFactory;
@@ -24,10 +25,12 @@
 import com.android.tools.r8.graph.TreeFixerBase;
 import com.android.tools.r8.ir.code.NumberGenerator;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.KeepInfoCollection;
 import com.android.tools.r8.shaking.MainDexInfo;
 import com.android.tools.r8.synthesis.SyntheticNaming.SyntheticKind;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.IterableUtils;
+import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.SetUtils;
 import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeHashMap;
 import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeMap;
@@ -36,6 +39,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
 import com.google.common.hash.HashCode;
 import java.util.ArrayList;
@@ -47,6 +51,8 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
 
 public class SyntheticFinalization {
 
@@ -123,43 +129,6 @@
     }
   }
 
-  public static class EquivalenceGroup<T extends SyntheticDefinition<?, T, ?>> {
-    private final List<T> members;
-
-    public EquivalenceGroup(T representative, List<T> members) {
-      assert !members.isEmpty();
-      assert members.get(0) == representative;
-      this.members = members;
-    }
-
-    public T getRepresentative() {
-      return members.get(0);
-    }
-
-    public List<T> getMembers() {
-      return members;
-    }
-
-    public int compareToIncludingContext(
-        EquivalenceGroup<T> other,
-        GraphLens graphLens,
-        ClassToFeatureSplitMap classToFeatureSplitMap,
-        SyntheticItems syntheticItems) {
-      return getRepresentative()
-          .compareTo(
-              other.getRepresentative(), true, graphLens, classToFeatureSplitMap, syntheticItems);
-    }
-
-    @Override
-    public String toString() {
-      return "EquivalenceGroup{ members = "
-          + members.size()
-          + ", repr = "
-          + getRepresentative()
-          + " }";
-    }
-  }
-
   private final InternalOptions options;
   private final SyntheticItems synthetics;
   private final CommittedSyntheticsCollection committed;
@@ -257,9 +226,15 @@
 
     SyntheticFinalizationGraphLens syntheticFinalizationGraphLens = lensBuilder.build(appView);
 
-    ImmutableSet.Builder<DexType> finalInputSyntheticsBuilder = ImmutableSet.builder();
-    committed.forEachSyntheticInput(
-        type -> finalInputSyntheticsBuilder.add(syntheticFinalizationGraphLens.lookupType(type)));
+    ImmutableSet<DexType> finalInputSynthetics =
+        syntheticFinalizationGraphLens != null
+            ? SetUtils.newImmutableSet(
+                builder ->
+                    committed.forEachSyntheticInput(
+                        syntheticInputType ->
+                            builder.accept(
+                                syntheticFinalizationGraphLens.lookupType(syntheticInputType))))
+            : committed.syntheticInputs;
 
     // TODO(b/181858113): Remove once deprecated main-dex-list is removed.
     MainDexInfo.Builder mainDexInfoBuilder = appView.appInfo().getMainDexInfo().builderFromCopy();
@@ -270,10 +245,7 @@
             SyntheticItems.INVALID_ID_AFTER_SYNTHETIC_FINALIZATION,
             application,
             new CommittedSyntheticsCollection(
-                committed.getLegacyTypes(),
-                finalMethods,
-                finalClasses,
-                finalInputSyntheticsBuilder.build()),
+                committed.getLegacyTypes(), finalMethods, finalClasses, finalInputSynthetics),
             ImmutableList.of()),
         syntheticFinalizationGraphLens,
         PrunedItems.builder().setPrunedApp(application).addRemovedClasses(prunedSynthetics).build(),
@@ -390,18 +362,16 @@
           SyntheticMethodDefinition representative = syntheticGroup.getRepresentative();
           SynthesizingContext context = representative.getContext();
           context.registerPrefixRewriting(syntheticType, appView);
-          DexProgramClass representativeClass = representative.getHolder();
-          addSyntheticMarker(representative.getKind(), representativeClass, context, appView);
-          assert representativeClass.getMethodCollection().size() == 1;
-          for (SyntheticMethodDefinition member : syntheticGroup.getMembers()) {
-            if (member != representative) {
-              pruned.add(member.getHolder());
-              deduplicatedClasses.add(member.getHolder());
-            }
-            if (member.getContext().isDerivedFromMainDexList(mainDexInfo)) {
-              derivedMainDexSynthetics.add(syntheticType);
-            }
+          addSyntheticMarker(
+              representative.getKind(), representative.getHolder(), context, appView);
+          if (syntheticGroup.isDerivedFromMainDexList(mainDexInfo)) {
+            derivedMainDexSynthetics.add(syntheticType);
           }
+          syntheticGroup.forEachNonRepresentativeMember(
+              member -> {
+                pruned.add(member.getHolder());
+                deduplicatedClasses.add(member.getHolder());
+              });
         });
 
     syntheticClassGroups.forEach(
@@ -411,16 +381,14 @@
           context.registerPrefixRewriting(syntheticType, appView);
           addSyntheticMarker(
               representative.getKind(), representative.getHolder(), context, appView);
-          for (SyntheticProgramClassDefinition member : syntheticGroup.getMembers()) {
-            DexProgramClass memberClass = member.getHolder();
-            if (member != representative) {
-              pruned.add(memberClass);
-              deduplicatedClasses.add(memberClass);
-            }
-            if (member.getContext().isDerivedFromMainDexList(mainDexInfo)) {
-              derivedMainDexSynthetics.add(syntheticType);
-            }
+          if (syntheticGroup.isDerivedFromMainDexList(mainDexInfo)) {
+            derivedMainDexSynthetics.add(syntheticType);
           }
+          syntheticGroup.forEachNonRepresentativeMember(
+              member -> {
+                pruned.add(member.getHolder());
+                deduplicatedClasses.add(member.getHolder());
+              });
         });
 
     // Only create a new application if anything changed.
@@ -454,6 +422,8 @@
     syntheticClassGroups.forEach(
         (syntheticType, syntheticGroup) -> {
           DexProgramClass externalSyntheticClass = appForLookup.programDefinitionFor(syntheticType);
+          assert externalSyntheticClass != null
+              : "Expected definition for " + syntheticType.getTypeName();
           SyntheticProgramClassDefinition representative = syntheticGroup.getRepresentative();
           addFinalSyntheticClass.accept(
               externalSyntheticClass,
@@ -493,10 +463,10 @@
       boolean verifyNonRepresentativesRemovedFromApplication(
           DexApplication application, Map<DexType, EquivalenceGroup<T>> syntheticGroups) {
     for (EquivalenceGroup<?> syntheticGroup : syntheticGroups.values()) {
-      for (SyntheticDefinition<?, ?, ?> member : syntheticGroup.getMembers()) {
-        assert member == syntheticGroup.getRepresentative()
-            || application.definitionFor(member.getHolder().getType()) == null;
-      }
+      syntheticGroup.forEachNonRepresentativeMember(
+          member -> {
+            assert application.definitionFor(member.getHolder().getType()) == null;
+          });
     }
     return true;
   }
@@ -529,34 +499,35 @@
           ClassToFeatureSplitMap classToFeatureSplitMap,
           Builder lensBuilder) {
     Map<String, List<EquivalenceGroup<T>>> groupsPerPrefix = new HashMap<>();
+    Map<DexType, EquivalenceGroup<T>> equivalences = new IdentityHashMap<>();
     potentialEquivalences.forEach(
         members -> {
-          List<List<T>> groups =
-              groupEquivalent(
-                  members, intermediate, appView.graphLens(), classToFeatureSplitMap, synthetics);
-          for (List<T> group : groups) {
-            T representative =
-                findDeterministicRepresentative(
-                    group, appView.graphLens(), classToFeatureSplitMap, synthetics);
-            // The representative is required to be the first element of the group.
-            group.remove(representative);
-            group.add(0, representative);
-            groupsPerPrefix
-                .computeIfAbsent(
-                    representative.getPrefixForExternalSyntheticType(), k -> new ArrayList<>())
-                .add(new EquivalenceGroup<>(representative, group));
+          List<EquivalenceGroup<T>> groups =
+              groupEquivalent(appView, members, intermediate, classToFeatureSplitMap);
+          for (EquivalenceGroup<T> group : groups) {
+            // If the group already has a representative, then this representative is pinned.
+            // Otherwise, we select a deterministic representative.
+            if (group.hasRepresentative()) {
+              EquivalenceGroup<T> previous =
+                  equivalences.put(group.getRepresentative().getHolder().getType(), group);
+              assert previous == null;
+            } else {
+              group.selectDeterministicRepresentative();
+              groupsPerPrefix
+                  .computeIfAbsent(
+                      group.getRepresentative().getPrefixForExternalSyntheticType(),
+                      k -> new ArrayList<>())
+                  .add(group);
+            }
           }
         });
-
-    Map<DexType, EquivalenceGroup<T>> equivalences = new IdentityHashMap<>();
     groupsPerPrefix.forEach(
         (externalSyntheticTypePrefix, groups) -> {
           // Sort the equivalence groups that go into 'context' including the context type of the
           // representative which is equal to 'context' here (see assert below).
           groups.sort(
               (a, b) ->
-                  a.compareToIncludingContext(
-                      b, appView.graphLens(), classToFeatureSplitMap, synthetics));
+                  a.compareToIncludingContext(b, appView.graphLens(), classToFeatureSplitMap));
           for (int i = 0; i < groups.size(); i++) {
             EquivalenceGroup<T> group = groups.get(i);
             assert group
@@ -567,84 +538,114 @@
             // of the synthetic name will be non-deterministic between the two.
             assert i == 0
                 || checkGroupsAreDistinct(
-                    groups.get(i - 1),
-                    group,
-                    appView.graphLens(),
-                    classToFeatureSplitMap,
-                    synthetics);
-            SyntheticKind kind = group.members.get(0).getKind();
+                    groups.get(i - 1), group, appView.graphLens(), classToFeatureSplitMap);
+            SyntheticKind kind = group.getRepresentative().getKind();
             DexType representativeType =
-                createExternalType(kind, externalSyntheticTypePrefix, generators, appView);
+                createExternalType(
+                    kind,
+                    externalSyntheticTypePrefix,
+                    generators,
+                    appView,
+                    equivalences::containsKey);
             equivalences.put(representativeType, group);
-            for (T member : group.getMembers()) {
-              lensBuilder.move(member.getHolder().getType(), representativeType);
-            }
           }
         });
+    equivalences.forEach(
+        (representativeType, group) ->
+            group.forEach(
+                member -> lensBuilder.move(member.getHolder().getType(), representativeType)));
     return equivalences;
   }
 
-  private static <T extends SyntheticDefinition<?, T, ?>> List<List<T>> groupEquivalent(
+  private static <T extends SyntheticDefinition<?, T, ?>> List<EquivalenceGroup<T>> groupEquivalent(
+      AppView<?> appView,
       List<T> potentialEquivalence,
       boolean intermediate,
-      GraphLens graphLens,
-      ClassToFeatureSplitMap classToFeatureSplitMap,
-      SyntheticItems syntheticItems) {
-    List<List<T>> groups = new ArrayList<>();
+      ClassToFeatureSplitMap classToFeatureSplitMap) {
+    List<EquivalenceGroup<T>> groups = new ArrayList<>();
     // Each other member is in a shared group if it is actually equivalent to the first member.
     for (T synthetic : potentialEquivalence) {
-      boolean requireNewGroup = true;
-      for (List<T> group : groups) {
+      boolean mustBeRepresentative = isPinned(appView, synthetic);
+      EquivalenceGroup<T> equivalenceGroup = null;
+      for (EquivalenceGroup<T> group : groups) {
         if (synthetic.isEquivalentTo(
-            group.get(0), intermediate, graphLens, classToFeatureSplitMap, syntheticItems)) {
-          requireNewGroup = false;
-          group.add(synthetic);
+            group.hasRepresentative()
+                ? group.getRepresentative()
+                : group.getFirstNonRepresentativeMember(),
+            intermediate,
+            appView.graphLens(),
+            classToFeatureSplitMap)) {
+          if (mustBeRepresentative && group.hasRepresentative()) {
+            // Check if the current synthetic is smaller than the group's representative. If so,
+            // then replace the representative, to ensure deterministic groups, and create a new
+            // singleton group containing the old representative. Otherwise, just add a singleton
+            // group containing the new synthetic.
+            T representative = group.getRepresentative();
+            if (representative
+                    .toReference()
+                    .getReference()
+                    .compareTo(synthetic.toReference().getReference())
+                > 0) {
+              group.replaceAndRemoveRepresentative(synthetic);
+              synthetic = representative;
+            }
+          } else {
+            equivalenceGroup = group;
+          }
           break;
         }
       }
-      if (requireNewGroup) {
-        List<T> newGroup = new ArrayList<>();
-        newGroup.add(synthetic);
-        groups.add(newGroup);
+      if (equivalenceGroup != null) {
+        equivalenceGroup.add(synthetic, mustBeRepresentative);
+      } else {
+        groups.add(new EquivalenceGroup<>(synthetic, mustBeRepresentative));
       }
     }
     return groups;
   }
 
+  /**
+   * In R8, keep rules may apply to synthetics from the input, if the input has been compiled using
+   * intermediate mode.
+   */
+  private static <D extends SyntheticDefinition<?, D, ?>> boolean isPinned(
+      AppView<?> appView, D definition) {
+    if (!appView.enableWholeProgramOptimizations()) {
+      return false;
+    }
+    if (!definition.getHolder().isProgramClass()) {
+      return true;
+    }
+    DexProgramClass holder = definition.getHolder().asProgramClass();
+    KeepInfoCollection keepInfo = appView.getKeepInfo();
+    if (keepInfo.getClassInfo(holder).isPinned()) {
+      return true;
+    }
+    for (DexEncodedMember<?, ?> member : holder.members()) {
+      if (keepInfo.getMemberInfo(member, holder).isPinned()) {
+        return true;
+      }
+    }
+    return false;
+  }
+
   private static <T extends SyntheticDefinition<?, T, ?>> boolean checkGroupsAreDistinct(
       EquivalenceGroup<T> g1,
       EquivalenceGroup<T> g2,
       GraphLens graphLens,
-      ClassToFeatureSplitMap classToFeatureSplitMap,
-      SyntheticItems syntheticItems) {
-    int order = g1.compareToIncludingContext(g2, graphLens, classToFeatureSplitMap, syntheticItems);
+      ClassToFeatureSplitMap classToFeatureSplitMap) {
+    int order = g1.compareToIncludingContext(g2, graphLens, classToFeatureSplitMap);
     assert order != 0;
-    assert order
-        != g2.compareToIncludingContext(g1, graphLens, classToFeatureSplitMap, syntheticItems);
+    assert order != g2.compareToIncludingContext(g1, graphLens, classToFeatureSplitMap);
     return true;
   }
 
-  private static <T extends SyntheticDefinition<?, T, ?>> T findDeterministicRepresentative(
-      List<T> members,
-      GraphLens graphLens,
-      ClassToFeatureSplitMap classToFeatureSplitMap,
-      SyntheticItems syntheticItems) {
-    // Pick a deterministic member as representative.
-    T smallest = members.get(0);
-    for (int i = 1; i < members.size(); i++) {
-      T next = members.get(i);
-      if (next.toReference().getReference().compareTo(smallest.toReference().getReference()) < 0) {
-        smallest = next;
-      }
-    }
-    return smallest;
-  }
-
   private DexType createExternalType(
       SyntheticKind kind,
       String externalSyntheticTypePrefix,
       Map<String, NumberGenerator> generators,
-      AppView<?> appView) {
+      AppView<?> appView,
+      Predicate<DexType> reserved) {
     DexItemFactory factory = appView.dexItemFactory();
     if (kind.isFixedSuffixSynthetic) {
       return SyntheticNaming.createExternalType(kind, externalSyntheticTypePrefix, "", factory);
@@ -656,6 +657,12 @@
       externalType =
           SyntheticNaming.createExternalType(
               kind, externalSyntheticTypePrefix, Integer.toString(generator.next()), factory);
+      // If the generated external type matches an external synthetic from the input, which is kept,
+      // then continue.
+      if (reserved.test(externalType)) {
+        externalType = null;
+        continue;
+      }
       DexClass clazz = appView.appInfo().definitionForWithoutExistenceAssert(externalType);
       if (clazz != null && isNotSyntheticType(clazz.type)) {
         assert options.testing.allowConflictingSyntheticTypes
@@ -724,4 +731,101 @@
     }
     return definitions;
   }
+
+  private static class EquivalenceGroup<T extends SyntheticDefinition<?, T, ?>> {
+
+    // The members of the equivalence group, *excluding* the representative.
+    private List<T> members = new ArrayList<>();
+    private T representative;
+
+    EquivalenceGroup(T member, boolean isRepresentative) {
+      add(member, isRepresentative);
+    }
+
+    void add(T member, boolean isRepresentative) {
+      if (isRepresentative) {
+        assert !hasRepresentative();
+        representative = member;
+      } else {
+        members.add(member);
+      }
+    }
+
+    int compareToIncludingContext(
+        EquivalenceGroup<T> other,
+        GraphLens graphLens,
+        ClassToFeatureSplitMap classToFeatureSplitMap) {
+      return getRepresentative()
+          .compareTo(other.getRepresentative(), true, graphLens, classToFeatureSplitMap);
+    }
+
+    public void forEach(Consumer<T> consumer) {
+      consumer.accept(getRepresentative());
+      members.forEach(consumer);
+    }
+
+    public void forEachNonRepresentativeMember(Consumer<T> consumer) {
+      members.forEach(consumer);
+    }
+
+    T getFirstNonRepresentativeMember() {
+      assert !members.isEmpty();
+      return members.get(0);
+    }
+
+    T getRepresentative() {
+      assert hasRepresentative();
+      return representative;
+    }
+
+    boolean hasRepresentative() {
+      return representative != null;
+    }
+
+    boolean isDerivedFromMainDexList(MainDexInfo mainDexInfo) {
+      return getRepresentative().getContext().isDerivedFromMainDexList(mainDexInfo)
+          || Iterables.any(
+              members, member -> member.getContext().isDerivedFromMainDexList(mainDexInfo));
+    }
+
+    void replaceAndRemoveRepresentative(T representative) {
+      assert hasRepresentative();
+      this.representative = representative;
+    }
+
+    void selectDeterministicRepresentative() {
+      // Pick a deterministic member as representative.
+      assert !hasRepresentative();
+      int representativeIndex = 0;
+      for (int i = 1; i < members.size(); i++) {
+        T next = members.get(i);
+        T representative = members.get(representativeIndex);
+        if (next.toReference().getReference().compareTo(representative.toReference().getReference())
+            < 0) {
+          representativeIndex = i;
+        }
+      }
+      T representative = members.get(representativeIndex);
+      members.set(representativeIndex, ListUtils.last(members));
+      ListUtils.removeLast(members);
+      setRepresentative(representative);
+    }
+
+    void setRepresentative(T representative) {
+      assert !hasRepresentative();
+      this.representative = representative;
+    }
+
+    @Override
+    public String toString() {
+      if (hasRepresentative()) {
+        return "EquivalenceGroup{ size = "
+            + (members.size() + 1)
+            + ", repr = "
+            + getRepresentative()
+            + " }";
+      }
+      return "EquivalenceGroup{ size = " + members.size() + " }";
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
index befe594..f709533 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
@@ -249,7 +249,7 @@
 
   public boolean isSubjectToKeepRules(DexProgramClass clazz) {
     assert isSyntheticClass(clazz);
-    return committed.containsSyntheticInput(clazz.getType());
+    return isSyntheticInput(clazz);
   }
 
   public boolean isSyntheticClass(DexType type) {
@@ -260,20 +260,37 @@
     return isSyntheticClass(clazz.type);
   }
 
+  boolean isSyntheticInput(DexProgramClass clazz) {
+    return committed.containsSyntheticInput(clazz.getType());
+  }
+
   public FeatureSplit getContextualFeatureSplit(DexType type) {
+    if (pending.legacyClasses.containsKey(type)) {
+      LegacySyntheticDefinition definition = pending.legacyClasses.get(type);
+      return definition.getFeatureSplit();
+    }
+    if (committed.containsLegacyType(type)) {
+      List<LegacySyntheticReference> types = committed.getLegacyTypes(type);
+      if (types.isEmpty()) {
+        return null;
+      }
+      assert verifyAllHaveSameFeature(types, LegacySyntheticReference::getFeatureSplit);
+      return types.get(0).getFeatureSplit();
+    }
     List<SynthesizingContext> contexts = getSynthesizingContexts(type);
     if (contexts.isEmpty()) {
       return null;
     }
-    assert verifyAllContextsHaveSameFeature(contexts);
+    assert verifyAllHaveSameFeature(contexts, SynthesizingContext::getFeatureSplit);
     return contexts.get(0).getFeatureSplit();
   }
 
-  private boolean verifyAllContextsHaveSameFeature(List<SynthesizingContext> contexts) {
-    assert !contexts.isEmpty();
-    FeatureSplit featureSplit = contexts.get(0).getFeatureSplit();
-    for (int i = 1; i < contexts.size(); i++) {
-      assert featureSplit == contexts.get(i).getFeatureSplit();
+  private static <T> boolean verifyAllHaveSameFeature(
+      List<T> items, Function<T, FeatureSplit> getter) {
+    assert !items.isEmpty();
+    FeatureSplit featureSplit = getter.apply(items.get(0));
+    for (int i = 1; i < items.size(); i++) {
+      assert featureSplit == getter.apply(items.get(i));
     }
     return true;
   }
@@ -379,9 +396,10 @@
   }
 
   // TODO(b/158159959): Remove the usage of this direct class addition.
-  public void addLegacySyntheticClass(DexProgramClass clazz, ProgramDefinition context) {
+  public void addLegacySyntheticClass(
+      DexProgramClass clazz, ProgramDefinition context, FeatureSplit featureSplit) {
     LegacySyntheticDefinition legacyItem = internalAddLegacySyntheticClass(clazz);
-    legacyItem.addContext(context);
+    legacyItem.addContext(context, featureSplit);
   }
 
   private LegacySyntheticDefinition internalAddLegacySyntheticClass(DexProgramClass clazz) {
diff --git a/src/main/java/com/android/tools/r8/utils/ListUtils.java b/src/main/java/com/android/tools/r8/utils/ListUtils.java
index e7c8e46..7b4976c 100644
--- a/src/main/java/com/android/tools/r8/utils/ListUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ListUtils.java
@@ -135,6 +135,12 @@
     return mapOrElse(list, fn, list);
   }
 
+  public static <T> ArrayList<T> newArrayList(T element) {
+    ArrayList<T> list = new ArrayList<>();
+    list.add(element);
+    return list;
+  }
+
   public static <T> ArrayList<T> newArrayList(ForEachable<T> forEachable) {
     ArrayList<T> list = new ArrayList<>();
     forEachable.forEach(list::add);
@@ -155,6 +161,10 @@
     return Optional.empty();
   }
 
+  public static <T> T removeLast(List<T> list) {
+    return list.remove(list.size() - 1);
+  }
+
   public static <T extends Comparable<T>> boolean verifyListIsOrdered(List<T> list) {
     for (int i = list.size() - 1; i > 0; i--) {
       if (list.get(i).compareTo(list.get(i - 1)) < 0) {
diff --git a/src/main/java/com/android/tools/r8/utils/SetUtils.java b/src/main/java/com/android/tools/r8/utils/SetUtils.java
index a278196..98ea2c4 100644
--- a/src/main/java/com/android/tools/r8/utils/SetUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/SetUtils.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.utils;
 
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import java.util.Collections;
 import java.util.IdentityHashMap;
@@ -55,6 +56,12 @@
     return result;
   }
 
+  public static <T> ImmutableSet<T> newImmutableSet(ForEachable<T> forEachable) {
+    ImmutableSet.Builder<T> builder = ImmutableSet.builder();
+    forEachable.forEach(builder::add);
+    return builder.build();
+  }
+
   public static <T, S> Set<T> mapIdentityHashSet(Set<S> set, Function<S, T> fn) {
     Set<T> out = newIdentityHashSet(set.size());
     for (S element : set) {
diff --git a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
index 97a95be8..121e7c4 100644
--- a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
@@ -65,6 +65,7 @@
   private MainDexClassesCollector mainDexClassesCollector;
   private StringConsumer mainDexListConsumer;
   protected int minApiLevel = ToolHelper.getMinApiLevelForDexVm().getLevel();
+  private boolean optimizeMultidexForLinearAlloc = false;
   private Consumer<InternalOptions> optionsConsumer = DEFAULT_OPTIONS;
   private ByteArrayOutputStream stdout = null;
   private PrintStream oldStdout = null;
@@ -167,6 +168,7 @@
           : "Don't set the API level directly through BaseCompilerCommand.Builder in tests";
       builder.setMinApiLevel(minApiLevel);
     }
+    builder.setOptimizeMultidexForLinearAlloc(optimizeMultidexForLinearAlloc);
     if (useDefaultRuntimeLibrary) {
       if (backend == Backend.DEX) {
         assert builder.isMinApiLevelSet();
@@ -308,6 +310,11 @@
     return self();
   }
 
+  public T setOptimizeMultidexForLinearAlloc() {
+    this.optimizeMultidexForLinearAlloc = true;
+    return self();
+  }
+
   public T disableDesugaring() {
     builder.setDisableDesugaring(true);
     return self();
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/CollisionWithDefaultMethodOutsideMergeGroupTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/CollisionWithDefaultMethodOutsideMergeGroupTest.java
new file mode 100644
index 0000000..ae64e0e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/CollisionWithDefaultMethodOutsideMergeGroupTest.java
@@ -0,0 +1,108 @@
+// 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.classmerging.horizontal.interfaces;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isImplementing;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
+import com.android.tools.r8.NoUnusedInterfaceRemoval;
+import com.android.tools.r8.NoVerticalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.HorizontallyMergedClassesInspector;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class CollisionWithDefaultMethodOutsideMergeGroupTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public CollisionWithDefaultMethodOutsideMergeGroupTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        // I and J are not eligible for merging, since class A (implements I) inherits a default m()
+        // method from K, which is also on J.
+        .addHorizontallyMergedClassesInspector(
+            HorizontallyMergedClassesInspector::assertNoClassesMerged)
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
+        .enableNoUnusedInterfaceRemovalAnnotations()
+        .enableNoVerticalClassMergingAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject aClassSubject = inspector.clazz(A.class);
+              assertThat(aClassSubject, isPresent());
+              assertThat(aClassSubject, isImplementing(inspector.clazz(I.class)));
+              assertThat(aClassSubject, isImplementing(inspector.clazz(K.class)));
+
+              ClassSubject bClassSubject = inspector.clazz(B.class);
+              assertThat(bClassSubject, isPresent());
+              assertThat(bClassSubject, isImplementing(inspector.clazz(J.class)));
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("K", "J");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      new A().m();
+      new B().m();
+    }
+  }
+
+  @NoUnusedInterfaceRemoval
+  @NoVerticalClassMerging
+  interface I {}
+
+  @NoUnusedInterfaceRemoval
+  @NoVerticalClassMerging
+  interface J {
+    @NeverInline
+    default void m() {
+      System.out.println("J");
+    }
+  }
+
+  @NoHorizontalClassMerging
+  @NoUnusedInterfaceRemoval
+  @NoVerticalClassMerging
+  interface K {
+    @NeverInline
+    default void m() {
+      System.out.println("K");
+    }
+  }
+
+  @NeverClassInline
+  @NoHorizontalClassMerging
+  static class A implements I, K {}
+
+  @NeverClassInline
+  @NoHorizontalClassMerging
+  static class B implements J {}
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointFunctionalInterfacesMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointFunctionalInterfacesMergingTest.java
new file mode 100644
index 0000000..0fd3ab3
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointFunctionalInterfacesMergingTest.java
@@ -0,0 +1,56 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.classmerging.horizontal.interfaces;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class DisjointFunctionalInterfacesMergingTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public DisjointFunctionalInterfacesMergingTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addHorizontallyMergedClassesInspector(
+            inspector -> inspector.assertClassesNotMerged(I.class, J.class))
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("I", "J");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      ((I) () -> System.out.println("I")).f();
+      ((J) () -> System.out.println("J")).g();
+    }
+  }
+
+  interface I {
+    void f();
+  }
+
+  interface J {
+    void g();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointInterfacesWithDefaultMethodsMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointInterfacesWithDefaultMethodsMergingTest.java
new file mode 100644
index 0000000..c9a0d19
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointInterfacesWithDefaultMethodsMergingTest.java
@@ -0,0 +1,91 @@
+// 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.classmerging.horizontal.interfaces;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isImplementing;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoUnusedInterfaceRemoval;
+import com.android.tools.r8.NoVerticalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.HorizontallyMergedClassesInspector;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class DisjointInterfacesWithDefaultMethodsMergingTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public DisjointInterfacesWithDefaultMethodsMergingTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        // TODO(b/173990042): I and J should be merged.
+        .addHorizontallyMergedClassesInspector(
+            HorizontallyMergedClassesInspector::assertNoClassesMerged)
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .enableNoUnusedInterfaceRemovalAnnotations()
+        .enableNoVerticalClassMergingAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject aClassSubject = inspector.clazz(A.class);
+              assertThat(aClassSubject, isPresent());
+              assertThat(aClassSubject, isImplementing(inspector.clazz(I.class)));
+              assertThat(aClassSubject, isImplementing(inspector.clazz(J.class)));
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("I", "J");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      new A().f();
+      new A().g();
+    }
+  }
+
+  @NoUnusedInterfaceRemoval
+  @NoVerticalClassMerging
+  interface I {
+    @NeverInline
+    default void f() {
+      System.out.println("I");
+    }
+  }
+
+  @NoUnusedInterfaceRemoval
+  @NoVerticalClassMerging
+  interface J {
+    @NeverInline
+    default void g() {
+      System.out.println("J");
+    }
+  }
+
+  @NeverClassInline
+  static class A implements I, J {}
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointInterfacesWithoutDefaultMethodsMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointInterfacesWithoutDefaultMethodsMergingTest.java
new file mode 100644
index 0000000..9a9ccc3
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointInterfacesWithoutDefaultMethodsMergingTest.java
@@ -0,0 +1,97 @@
+// 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.classmerging.horizontal.interfaces;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isImplementing;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoUnusedInterfaceRemoval;
+import com.android.tools.r8.NoVerticalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.HorizontallyMergedClassesInspector;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class DisjointInterfacesWithoutDefaultMethodsMergingTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public DisjointInterfacesWithoutDefaultMethodsMergingTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        // TODO(b/173990042): I and J should be merged.
+        .addHorizontallyMergedClassesInspector(
+            HorizontallyMergedClassesInspector::assertNoClassesMerged)
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .enableNoUnusedInterfaceRemovalAnnotations()
+        .enableNoVerticalClassMergingAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject aClassSubject = inspector.clazz(A.class);
+              assertThat(aClassSubject, isPresent());
+              assertThat(aClassSubject, isImplementing(inspector.clazz(I.class)));
+              assertThat(aClassSubject, isImplementing(inspector.clazz(J.class)));
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("I", "J");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      new A().f();
+      new A().g();
+    }
+  }
+
+  @NoUnusedInterfaceRemoval
+  @NoVerticalClassMerging
+  interface I {
+    void f();
+  }
+
+  @NoUnusedInterfaceRemoval
+  @NoVerticalClassMerging
+  interface J {
+    void g();
+  }
+
+  @NeverClassInline
+  static class A implements I, J {
+    @NeverInline
+    @Override
+    public void f() {
+      System.out.println("I");
+    }
+
+    @NeverInline
+    @Override
+    public void g() {
+      System.out.println("J");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/EmptyInterfacesMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/EmptyInterfacesMergingTest.java
new file mode 100644
index 0000000..74aa943
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/EmptyInterfacesMergingTest.java
@@ -0,0 +1,75 @@
+// 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.classmerging.horizontal.interfaces;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isImplementing;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NoUnusedInterfaceRemoval;
+import com.android.tools.r8.NoVerticalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.HorizontallyMergedClassesInspector;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class EmptyInterfacesMergingTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public EmptyInterfacesMergingTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        // TODO(b/173990042): I and J should be merged.
+        .addHorizontallyMergedClassesInspector(
+            HorizontallyMergedClassesInspector::assertNoClassesMerged)
+        .enableNoUnusedInterfaceRemovalAnnotations()
+        .enableNoVerticalClassMergingAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject aClassSubject = inspector.clazz(A.class);
+              assertThat(aClassSubject, isPresent());
+              assertThat(aClassSubject, isImplementing(inspector.clazz(I.class)));
+              assertThat(aClassSubject, isImplementing(inspector.clazz(J.class)));
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccess();
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      System.out.println(A.class);
+    }
+  }
+
+  @NoUnusedInterfaceRemoval
+  @NoVerticalClassMerging
+  interface I {}
+
+  @NoUnusedInterfaceRemoval
+  @NoVerticalClassMerging
+  interface J {}
+
+  static class A implements I, J {}
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/NoDefaultMethodMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/NoDefaultMethodMergingTest.java
new file mode 100644
index 0000000..6a1b373
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/NoDefaultMethodMergingTest.java
@@ -0,0 +1,101 @@
+// 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.classmerging.horizontal.interfaces;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isImplementing;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
+import com.android.tools.r8.NoUnusedInterfaceRemoval;
+import com.android.tools.r8.NoVerticalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.HorizontallyMergedClassesInspector;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class NoDefaultMethodMergingTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public NoDefaultMethodMergingTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        // I and J are not eligible for merging, since they declare the same default method.
+        .addHorizontallyMergedClassesInspector(
+            HorizontallyMergedClassesInspector::assertNoClassesMerged)
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
+        .enableNoUnusedInterfaceRemovalAnnotations()
+        .enableNoVerticalClassMergingAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject aClassSubject = inspector.clazz(A.class);
+              assertThat(aClassSubject, isPresent());
+              assertThat(aClassSubject, isImplementing(inspector.clazz(I.class)));
+
+              ClassSubject bClassSubject = inspector.clazz(B.class);
+              assertThat(bClassSubject, isPresent());
+              assertThat(bClassSubject, isImplementing(inspector.clazz(J.class)));
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("I", "J");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      new A().m();
+      new B().m();
+    }
+  }
+
+  @NoUnusedInterfaceRemoval
+  @NoVerticalClassMerging
+  interface I {
+    @NeverInline
+    default void m() {
+      System.out.println("I");
+    }
+  }
+
+  @NoUnusedInterfaceRemoval
+  @NoVerticalClassMerging
+  interface J {
+    @NeverInline
+    default void m() {
+      System.out.println("J");
+    }
+  }
+
+  @NeverClassInline
+  @NoHorizontalClassMerging
+  static class A implements I {}
+
+  @NeverClassInline
+  @NoHorizontalClassMerging
+  static class B implements J {}
+}
diff --git a/src/test/java/com/android/tools/r8/internal/D8GMSCoreV10DeployJarVerificationTest.java b/src/test/java/com/android/tools/r8/internal/D8GMSCoreV10DeployJarVerificationTest.java
deleted file mode 100644
index 7f685c5..0000000
--- a/src/test/java/com/android/tools/r8/internal/D8GMSCoreV10DeployJarVerificationTest.java
+++ /dev/null
@@ -1,95 +0,0 @@
-// Copyright (c) 2017, 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.internal;
-
-import com.android.tools.r8.CompilationMode;
-import com.android.tools.r8.D8Command;
-import com.android.tools.r8.R8RunArtTestsTest.CompilerUnderTest;
-import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.utils.AndroidApiLevel;
-import com.android.tools.r8.utils.AndroidApp;
-import java.nio.file.Paths;
-import org.junit.Test;
-
-public class D8GMSCoreV10DeployJarVerificationTest extends GMSCoreDeployJarVerificationTest {
-
-  @Test
-  public void buildDebugFromDeployJar() throws Exception {
-    buildFromDeployJar(
-        CompilerUnderTest.D8, CompilationMode.DEBUG,
-        GMSCoreCompilationTestBase.GMSCORE_V10_DIR, false);
-  }
-
-  @Test
-  public void buildReleaseFromDeployJar() throws Exception {
-    buildFromDeployJar(
-        CompilerUnderTest.D8, CompilationMode.RELEASE,
-        GMSCoreCompilationTestBase.GMSCORE_V10_DIR, false);
-  }
-
-  @Test
-  public void testDeterminismDebugLegacyMultidexFromDeployJar() throws Exception {
-    D8Command.Builder builder =
-        D8Command.builder()
-            .addProgramFiles(Paths.get(GMSCORE_V10_DIR + DEPLOY_JAR))
-            .addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.K.getLevel()))
-            .setMode(CompilationMode.DEBUG)
-            .setMinApiLevel(AndroidApiLevel.K.getLevel())
-            .addMainDexListFiles(Paths.get(GMSCORE_V10_DIR + "main_dex_list.txt"));
-
-    AndroidApp app1 = runAndCheckVerification(builder, null);
-    D8Command.Builder builder2 =
-        D8Command.builder()
-            .addProgramFiles(Paths.get(GMSCORE_V10_DIR + DEPLOY_JAR))
-            .addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.K.getLevel()))
-            .setMode(CompilationMode.DEBUG)
-            .setMinApiLevel(AndroidApiLevel.K.getLevel())
-            .addMainDexListFiles(Paths.get(GMSCORE_V10_DIR + "main_dex_list.txt"));
-
-    AndroidApp app2 = runAndCheckVerification(builder2, null);
-    // Verify that the result of the two compilations was the same.
-    assertIdenticalApplications(app1, app2);
-  }
-
-  @Test
-  public void buildDebugLegagyMultidexForDexOptFromDeployJar() throws Exception {
-    D8Command.Builder builder =
-        D8Command.builder()
-            .addProgramFiles(Paths.get(GMSCORE_V10_DIR + DEPLOY_JAR))
-            .addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.K.getLevel()))
-            .setMode(CompilationMode.DEBUG)
-            .setMinApiLevel(AndroidApiLevel.K.getLevel())
-            .setOptimizeMultidexForLinearAlloc(true)
-            .addMainDexListFiles(Paths.get(GMSCORE_V10_DIR + "main_dex_list.txt"));
-
-    runAndCheckVerification(builder, null);
-  }
-
-  @Test
-  public void buildReleaseLegagyMultidexFromDeployJar() throws Exception {
-    D8Command.Builder builder =
-        D8Command.builder()
-            .addProgramFiles(Paths.get(GMSCORE_V10_DIR + DEPLOY_JAR))
-            .addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.K.getLevel()))
-            .setMode(CompilationMode.DEBUG)
-            .setMinApiLevel(AndroidApiLevel.K.getLevel())
-            .addMainDexListFiles(Paths.get(GMSCORE_V10_DIR + "main_dex_list.txt"));
-
-    runAndCheckVerification(builder, null);
-  }
-
-  @Test
-  public void buildReleaseLegagyMultidexForDexOptFromDeployJar() throws Exception {
-    D8Command.Builder builder =
-        D8Command.builder()
-            .addProgramFiles(Paths.get(GMSCORE_V10_DIR + DEPLOY_JAR))
-            .addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.K.getLevel()))
-            .setMode(CompilationMode.DEBUG)
-            .setMinApiLevel(AndroidApiLevel.K.getLevel())
-            .setOptimizeMultidexForLinearAlloc(true)
-            .addMainDexListFiles(Paths.get(GMSCORE_V10_DIR + "main_dex_list.txt"));
-
-    runAndCheckVerification(builder, null);
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/internal/D8GMSCoreV9DeployJarVerificationTest.java b/src/test/java/com/android/tools/r8/internal/D8GMSCoreV9DeployJarVerificationTest.java
deleted file mode 100644
index 25f4412..0000000
--- a/src/test/java/com/android/tools/r8/internal/D8GMSCoreV9DeployJarVerificationTest.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (c) 2017, 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.internal;
-
-import com.android.tools.r8.CompilationMode;
-import com.android.tools.r8.R8RunArtTestsTest.CompilerUnderTest;
-import org.junit.Test;
-
-public class D8GMSCoreV9DeployJarVerificationTest extends GMSCoreDeployJarVerificationTest {
-
-  @Test
-  public void buildDebugFromDeployJar() throws Exception {
-    buildFromDeployJar(
-        CompilerUnderTest.D8, CompilationMode.DEBUG,
-        GMSCoreCompilationTestBase.GMSCORE_V9_DIR, true);
-  }
-
-  @Test
-  public void buildReleaseFromDeployJar() throws Exception {
-    buildFromDeployJar(
-        CompilerUnderTest.D8, CompilationMode.RELEASE,
-        GMSCoreCompilationTestBase.GMSCORE_V9_DIR, true);
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/internal/GMSCoreCompilationTestBase.java b/src/test/java/com/android/tools/r8/internal/GMSCoreCompilationTestBase.java
index de9b402..4ed740b 100644
--- a/src/test/java/com/android/tools/r8/internal/GMSCoreCompilationTestBase.java
+++ b/src/test/java/com/android/tools/r8/internal/GMSCoreCompilationTestBase.java
@@ -3,61 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.internal;
 
-import com.android.tools.r8.CompilationFailedException;
-import com.android.tools.r8.CompilationMode;
-import com.android.tools.r8.R8RunArtTestsTest.CompilerUnderTest;
-import com.android.tools.r8.utils.InternalOptions;
-import com.google.common.collect.ImmutableList;
-import java.io.IOException;
-import java.nio.file.Paths;
-import java.util.concurrent.ExecutionException;
-import java.util.function.Consumer;
-
 public abstract class GMSCoreCompilationTestBase extends CompilationTestBase {
-  public static final String GMSCORE_V4_DIR = "third_party/gmscore/v4/";
-  public static final String GMSCORE_V5_DIR = "third_party/gmscore/v5/";
-  public static final String GMSCORE_V6_DIR = "third_party/gmscore/v6/";
-  public static final String GMSCORE_V7_DIR = "third_party/gmscore/v7/";
-  public static final String GMSCORE_V8_DIR = "third_party/gmscore/v8/";
-  public static final String GMSCORE_V9_DIR = "third_party/gmscore/gmscore_v9/";
-  public static final String GMSCORE_V10_DIR = "third_party/gmscore/gmscore_v10/";
-  public static final String GMSCORE_LATEST_DIR = "third_party/gmscore/latest/";
-
-  public static final int GMSCORE_V9_MAX_SIZE = 35000000;
-  public static final int GMSCORE_V10_MAX_SIZE = 35000000;
-  public static final int GMSCORE_LATEST_MAX_SIZE = 35000000;
-
-  static final String GMSCORE_APK = "GMSCore.apk";
-
   // Files pertaining to the full GMSCore build.
-  static final String PG_MAP = "GmsCore_prod_alldpi_release_all_locales_proguard.map";
   static final String PG_CONF = "GmsCore_prod_alldpi_release_all_locales_proguard.config";
   static final String DEPLOY_JAR = "GmsCore_prod_alldpi_release_all_locales_deploy.jar";
-  static final String REFERENCE_APK = "noshrink_x86_GmsCore_prod_alldpi_release_unsigned.apk";
-
-  public void runR8AndCheckVerification(CompilationMode mode, String version)
-      throws ExecutionException, IOException, CompilationFailedException {
-    runR8AndCheckVerification(mode, version, null);
-  }
-
-  public void runR8AndCheckVerification(
-      CompilationMode mode, String version, Consumer<InternalOptions> optionsConsumer)
-      throws ExecutionException, IOException, CompilationFailedException {
-    runAndCheckVerification(CompilerUnderTest.R8, mode, version, optionsConsumer);
-  }
-
-  private void runAndCheckVerification(
-      CompilerUnderTest compiler,
-      CompilationMode mode,
-      String version,
-      Consumer<InternalOptions> optionsConsumer)
-      throws ExecutionException, IOException, CompilationFailedException {
-    runAndCheckVerification(
-        compiler,
-        mode,
-        version + GMSCORE_APK,
-        null,
-        optionsConsumer,
-        ImmutableList.of(Paths.get(version, GMSCORE_APK).toString()));
-  }
 }
diff --git a/src/test/java/com/android/tools/r8/internal/GMSCoreDeployJarVerificationTest.java b/src/test/java/com/android/tools/r8/internal/GMSCoreDeployJarVerificationTest.java
deleted file mode 100644
index 634ff6f..0000000
--- a/src/test/java/com/android/tools/r8/internal/GMSCoreDeployJarVerificationTest.java
+++ /dev/null
@@ -1,59 +0,0 @@
-// Copyright (c) 2016, 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.internal;
-
-import com.android.tools.r8.CompilationFailedException;
-import com.android.tools.r8.CompilationMode;
-import com.android.tools.r8.DexIndexedConsumer;
-import com.android.tools.r8.R8RunArtTestsTest.CompilerUnderTest;
-import com.android.tools.r8.shaking.ProguardRuleParserException;
-import com.android.tools.r8.utils.AndroidApp;
-import com.android.tools.r8.utils.InternalOptions;
-import java.io.IOException;
-import java.util.Collections;
-import java.util.concurrent.ExecutionException;
-import java.util.function.Consumer;
-import java.util.function.Supplier;
-
-public class GMSCoreDeployJarVerificationTest extends GMSCoreCompilationTestBase {
-
-  public AndroidApp buildFromDeployJar(
-      CompilerUnderTest compiler, CompilationMode mode, String base, boolean hasReference)
-      throws ExecutionException, IOException, ProguardRuleParserException,
-      CompilationFailedException {
-    return runAndCheckVerification(
-        compiler, mode, hasReference ? base + REFERENCE_APK : null, null, base + DEPLOY_JAR);
-  }
-
-
-  public AndroidApp buildFromDeployJar(
-      CompilerUnderTest compiler, CompilationMode mode, String base, boolean hasReference,
-      Consumer<InternalOptions> optionsConsumer)
-      throws ExecutionException, IOException, ProguardRuleParserException,
-      CompilationFailedException {
-    return runAndCheckVerification(
-        compiler,
-        mode,
-        hasReference ? base + REFERENCE_APK : null,
-        null,
-        optionsConsumer,
-        Collections.singletonList(base + DEPLOY_JAR));
-  }
-
-  public AndroidApp buildFromDeployJar(
-      CompilerUnderTest compiler, CompilationMode mode, String base, boolean hasReference,
-      Consumer<InternalOptions> optionsConsumer, Supplier<DexIndexedConsumer> consumerSupplier)
-      throws ExecutionException, IOException, ProguardRuleParserException,
-      CompilationFailedException {
-    return runAndCheckVerification(
-        compiler,
-        mode,
-        hasReference ? base + REFERENCE_APK : null,
-        null,
-        optionsConsumer,
-        consumerSupplier,
-        Collections.singletonList(base + DEPLOY_JAR));
-  }
-
-}
diff --git a/src/test/java/com/android/tools/r8/internal/GMSCoreLatestTest.java b/src/test/java/com/android/tools/r8/internal/GMSCoreLatestTest.java
new file mode 100644
index 0000000..3beab34
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/internal/GMSCoreLatestTest.java
@@ -0,0 +1,133 @@
+// Copyright (c) 2019, 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.internal;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.core.AnyOf.anyOf;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import com.android.tools.r8.R8FullTestBuilder;
+import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ThrowableConsumer;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.AssertionUtils;
+import com.google.common.collect.Sets;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Collections;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class GMSCoreLatestTest extends GMSCoreCompilationTestBase {
+
+  private static final Path base = Paths.get("third_party/gmscore/latest/");
+
+  private static Path sanitizedLibrary;
+  private static Path sanitizedProguardConfiguration;
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters()
+        .withDexRuntime(Version.V9_0_0)
+        .withApiLevel(AndroidApiLevel.L)
+        .build();
+  }
+
+  public GMSCoreLatestTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @BeforeClass
+  public static void setup() throws Exception {
+    LibrarySanitizer librarySanitizer =
+        new LibrarySanitizer(getStaticTemp())
+            .addProguardConfigurationFiles(base.resolve(PG_CONF))
+            .sanitize();
+    sanitizedLibrary = librarySanitizer.getSanitizedLibrary();
+    sanitizedProguardConfiguration = librarySanitizer.getSanitizedProguardConfiguration();
+  }
+
+  @Test
+  public void testR8Determinism() throws Exception {
+    Map<String, String> idsRoundOne = new ConcurrentHashMap<>();
+    R8TestCompileResult compileResult =
+        compileWithR8(
+            builder ->
+                builder.addOptionsModification(
+                    options ->
+                        options.testing.processingContextsConsumer =
+                            id -> assertNull(idsRoundOne.put(id, id))));
+
+    compileResult.runDex2Oat(parameters.getRuntime()).assertNoVerificationErrors();
+
+    Map<String, String> idsRoundTwo = new ConcurrentHashMap<>();
+    R8TestCompileResult otherCompileResult =
+        compileWithR8(
+            builder ->
+                builder.addOptionsModification(
+                    options ->
+                        options.testing.processingContextsConsumer =
+                            id -> {
+                              AssertionUtils.assertNotNull(idsRoundOne.get(id));
+                              assertNull(idsRoundTwo.put(id, id));
+                            }));
+
+    // Verify that the result of the two compilations was the same.
+    assertEquals(
+        Collections.emptySet(),
+        Sets.symmetricDifference(idsRoundOne.keySet(), idsRoundTwo.keySet()));
+    assertIdenticalApplications(compileResult.getApp(), otherCompileResult.getApp());
+    assertEquals(compileResult.getProguardMap(), otherCompileResult.getProguardMap());
+  }
+
+  private R8TestCompileResult compileWithR8(ThrowableConsumer<R8FullTestBuilder> configuration)
+      throws Exception {
+    // Program files are included in Proguard configuration.
+    return testForR8(Backend.DEX)
+        .addLibraryFiles(sanitizedLibrary)
+        .addKeepRuleFiles(sanitizedProguardConfiguration)
+        .addDontWarn(
+            "android.hardware.location.IActivityRecognitionHardware",
+            "android.hardware.location.IFusedLocationHardware",
+            "android.location.FusedBatchOptions",
+            "android.location.GeocoderParams$1",
+            "android.location.ILocationManager",
+            "android.media.IRemoteDisplayCallback",
+            "android.media.RemoteDisplayState$RemoteDisplayInfo",
+            "com.android.internal.location.ProviderProperties",
+            "com.android.internal.location.ProviderRequest",
+            "com.google.protobuf.java_com_google_android_libraries_performance_primes_release"
+                + "_gmscore__primes_bcdd2915GeneratedExtensionRegistryLite$Loader")
+        .allowDiagnosticMessages()
+        .allowUnusedProguardConfigurationRules()
+        .setMinApi(parameters.getApiLevel())
+        .apply(configuration)
+        .compile()
+        .assertAllInfoMessagesMatch(
+            anyOf(
+                containsString("Ignoring option: -optimizations"),
+                containsString(
+                    "Invalid parameter counts in MethodParameter attributes. "
+                        + "This is likely due to Proguard having removed a parameter."),
+                containsString("Methods with invalid MethodParameter attributes:"),
+                containsString("Proguard configuration rule does not match anything")))
+        .assertAllWarningMessagesMatch(
+            anyOf(
+                containsString(
+                    "Expected stack map table for method with non-linear control flow. "
+                        + "In later version of R8, the method may be assumed not reachable."),
+                containsString("Ignoring option: -outjars")));
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/internal/GMSCoreV10Test.java b/src/test/java/com/android/tools/r8/internal/GMSCoreV10Test.java
new file mode 100644
index 0000000..30716fb
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/internal/GMSCoreV10Test.java
@@ -0,0 +1,212 @@
+// Copyright (c) 2017, 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.internal;
+
+import static com.android.tools.r8.utils.AssertionUtils.assertNotNull;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.core.AnyOf.anyOf;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import com.android.tools.r8.D8TestBuilder;
+import com.android.tools.r8.D8TestCompileResult;
+import com.android.tools.r8.R8FullTestBuilder;
+import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.TestCompilerBuilder;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ThrowableConsumer;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.google.common.collect.Sets;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Collections;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class GMSCoreV10Test extends GMSCoreCompilationTestBase {
+
+  private static final Path base = Paths.get("third_party/gmscore/gmscore_v10/");
+
+  private static Path sanitizedLibrary;
+  private static Path sanitizedProguardConfiguration;
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters()
+        .withDexRuntime(Version.V9_0_0)
+        .withApiLevel(AndroidApiLevel.L)
+        .build();
+  }
+
+  public GMSCoreV10Test(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @BeforeClass
+  public static void setup() throws Exception {
+    LibrarySanitizer librarySanitizer =
+        new LibrarySanitizer(getStaticTemp())
+            .addProguardConfigurationFiles(base.resolve(PG_CONF))
+            .sanitize();
+    sanitizedLibrary = librarySanitizer.getSanitizedLibrary();
+    sanitizedProguardConfiguration = librarySanitizer.getSanitizedProguardConfiguration();
+  }
+
+  @Test
+  public void testR8Determinism() throws Exception {
+    Map<String, String> idsRoundOne = new ConcurrentHashMap<>();
+    R8TestCompileResult compileResult =
+        compileWithR8(
+            builder ->
+                builder.addOptionsModification(
+                    options ->
+                        options.testing.processingContextsConsumer =
+                            id -> assertNull(idsRoundOne.put(id, id))));
+
+    compileResult.runDex2Oat(parameters.getRuntime()).assertNoVerificationErrors();
+
+    Map<String, String> idsRoundTwo = new ConcurrentHashMap<>();
+    R8TestCompileResult otherCompileResult =
+        compileWithR8(
+            builder ->
+                builder.addOptionsModification(
+                    options ->
+                        options.testing.processingContextsConsumer =
+                            id -> {
+                              assertNotNull(idsRoundOne.get(id));
+                              assertNull(idsRoundTwo.put(id, id));
+                            }));
+
+    // Verify that the result of the two compilations was the same.
+    assertEquals(
+        Collections.emptySet(),
+        Sets.symmetricDifference(idsRoundOne.keySet(), idsRoundTwo.keySet()));
+    assertIdenticalApplications(compileResult.getApp(), otherCompileResult.getApp());
+    assertEquals(compileResult.getProguardMap(), otherCompileResult.getProguardMap());
+  }
+
+  @Test
+  public void testR8ForceJumboStringProcessing() throws Exception {
+    compileWithR8(
+            builder ->
+                builder.addOptionsModification(
+                    options -> options.testing.forceJumboStringProcessing = true))
+        .runDex2Oat(parameters.getRuntime())
+        .assertNoVerificationErrors();
+  }
+
+  @Test
+  public void testD8Debug() throws Exception {
+    compileWithD8Debug(ThrowableConsumer.empty());
+  }
+
+  @Test
+  public void testD8DebugLegacyMultidex() throws Exception {
+    compileWithD8Debug(
+            builder ->
+                builder
+                    .addMainDexListFiles(base.resolve("main_dex_list.txt"))
+                    .setMinApi(AndroidApiLevel.K))
+        .runDex2Oat(parameters.getRuntime())
+        .assertNoVerificationErrors();
+  }
+
+  @Test
+  public void testD8DebugLegacyMultidexDexOpt() throws Exception {
+    compileWithD8Debug(
+            builder ->
+                builder
+                    .addMainDexListFiles(base.resolve("main_dex_list.txt"))
+                    .setMinApi(AndroidApiLevel.K)
+                    .setOptimizeMultidexForLinearAlloc())
+        .runDex2Oat(parameters.getRuntime())
+        .assertNoVerificationErrors();
+  }
+
+  @Test
+  public void testD8Release() throws Exception {
+    compileWithD8Release(ThrowableConsumer.empty())
+        .runDex2Oat(parameters.getRuntime())
+        .assertNoVerificationErrors();
+  }
+
+  @Test
+  public void testD8ReleaseLegacyMultidex() throws Exception {
+    compileWithD8Release(
+            builder ->
+                builder
+                    .addMainDexListFiles(base.resolve("main_dex_list.txt"))
+                    .setMinApi(AndroidApiLevel.K))
+        .runDex2Oat(parameters.getRuntime())
+        .assertNoVerificationErrors();
+  }
+
+  @Test
+  public void buildD8ReleaseLegacyMultidexDexOpt() throws Exception {
+    compileWithD8Release(
+            builder ->
+                builder
+                    .addMainDexListFiles(base.resolve("main_dex_list.txt"))
+                    .setMinApi(AndroidApiLevel.K)
+                    .setOptimizeMultidexForLinearAlloc())
+        .runDex2Oat(parameters.getRuntime())
+        .assertNoVerificationErrors();
+  }
+
+  private D8TestCompileResult compileWithD8Debug(ThrowableConsumer<D8TestBuilder> configuration)
+      throws Exception {
+    return compileWithD8(configuration.andThen(TestCompilerBuilder::debug));
+  }
+
+  private D8TestCompileResult compileWithD8Release(ThrowableConsumer<D8TestBuilder> configuration)
+      throws Exception {
+    return compileWithD8(configuration.andThen(TestCompilerBuilder::release));
+  }
+
+  private D8TestCompileResult compileWithD8(ThrowableConsumer<D8TestBuilder> configuration)
+      throws Exception {
+    return testForD8()
+        .addProgramFiles(base.resolve(DEPLOY_JAR))
+        .setMinApi(AndroidApiLevel.L)
+        .apply(configuration)
+        .compile();
+  }
+
+  private R8TestCompileResult compileWithR8(ThrowableConsumer<R8FullTestBuilder> configuration)
+      throws Exception {
+    // Program files are included in Proguard configuration.
+    return testForR8(Backend.DEX)
+        .addLibraryFiles(sanitizedLibrary)
+        .addKeepRuleFiles(sanitizedProguardConfiguration)
+        .addDontWarn(
+            "android.hardware.location.IActivityRecognitionHardware",
+            "android.hardware.location.IFusedLocationHardware",
+            "android.location.FusedBatchOptions",
+            "android.location.GeocoderParams$1",
+            "android.location.ILocationManager",
+            "android.media.IRemoteDisplayCallback",
+            "android.media.RemoteDisplayState$RemoteDisplayInfo",
+            "com.android.internal.location.ProviderProperties",
+            "com.android.internal.location.ProviderRequest")
+        .allowDiagnosticMessages()
+        .allowUnusedProguardConfigurationRules()
+        .setMinApi(parameters.getApiLevel())
+        .apply(configuration)
+        .compile()
+        .assertAllInfoMessagesMatch(
+            anyOf(
+                containsString("Ignoring option: -optimizations"),
+                containsString("Proguard configuration rule does not match anything")))
+        .assertAllWarningMessagesMatch(containsString("Ignoring option: -outjars"));
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/internal/LibrarySanitizer.java b/src/test/java/com/android/tools/r8/internal/LibrarySanitizer.java
index 4a256bb..e43dd8e 100644
--- a/src/test/java/com/android/tools/r8/internal/LibrarySanitizer.java
+++ b/src/test/java/com/android/tools/r8/internal/LibrarySanitizer.java
@@ -14,6 +14,7 @@
 import java.io.IOException;
 import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import org.junit.rules.TemporaryFolder;
 
@@ -56,6 +57,10 @@
     return this;
   }
 
+  LibrarySanitizer addProguardConfigurationFiles(Path... proguardConfigurationFiles) {
+    return addProguardConfigurationFiles(Arrays.asList(proguardConfigurationFiles));
+  }
+
   public Path getSanitizedLibrary() {
     return sanitizedLibrary;
   }
diff --git a/src/test/java/com/android/tools/r8/internal/R8GMSCoreDeterministicTest.java b/src/test/java/com/android/tools/r8/internal/R8GMSCoreDeterministicTest.java
deleted file mode 100644
index cba792d..0000000
--- a/src/test/java/com/android/tools/r8/internal/R8GMSCoreDeterministicTest.java
+++ /dev/null
@@ -1,70 +0,0 @@
-// Copyright (c) 2016, 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.internal;
-
-import static junit.framework.TestCase.assertEquals;
-
-import com.android.tools.r8.CompilationFailedException;
-import com.android.tools.r8.DexIndexedConsumer;
-import com.android.tools.r8.OutputMode;
-import com.android.tools.r8.R8Command;
-import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.utils.AndroidApiLevel;
-import com.android.tools.r8.utils.AndroidApp;
-import com.android.tools.r8.utils.IROrdering.NondeterministicIROrdering;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import org.junit.Test;
-
-public class R8GMSCoreDeterministicTest extends GMSCoreCompilationTestBase {
-
-  private static class CompilationResult {
-    AndroidApp app;
-    String proguardMap;
-  }
-
-  private CompilationResult doRun() throws CompilationFailedException {
-    R8Command command =
-        R8Command.builder()
-            .setDisableTreeShaking(true)
-            .setDisableMinification(true)
-            .addProgramFiles(Paths.get(GMSCORE_V7_DIR, GMSCORE_APK))
-            .setProgramConsumer(DexIndexedConsumer.emptyConsumer())
-            .setMinApiLevel(AndroidApiLevel.L.getLevel())
-            .build();
-    CompilationResult result = new CompilationResult();
-    result.app =
-        ToolHelper.runR8(
-            command,
-            options -> {
-              // For this test just do random shuffle.
-              options.testing.irOrdering = NondeterministicIROrdering.getInstance();
-              // Only use one thread to process to process in the order decided by the callback.
-              options.threadCount = 1;
-              // Ignore the missing classes.
-              options.ignoreMissingClasses = true;
-              // Store the generated Proguard map.
-              options.proguardMapConsumer =
-                  ToolHelper.consumeString(proguardMap -> result.proguardMap = proguardMap);
-            });
-    return result;
-  }
-
-  @Test
-  public void deterministic() throws Exception {
-    // Run two independent compilations.
-    CompilationResult result1 = doRun();
-    CompilationResult result2 = doRun();
-
-    // Check that the generated bytecode runs through the dex2oat verifier with no errors.
-    Path combinedInput = temp.getRoot().toPath().resolve("all.jar");
-    Path oatFile = temp.getRoot().toPath().resolve("all.oat");
-    result1.app.writeToZip(combinedInput, OutputMode.DexIndexed);
-    ToolHelper.runDex2Oat(combinedInput, oatFile);
-
-    // Verify that the result of the two compilations was the same.
-    assertIdenticalApplications(result1.app, result2.app);
-    assertEquals(result1.proguardMap, result2.proguardMap);
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/internal/R8GMSCoreFixedPointTest.java b/src/test/java/com/android/tools/r8/internal/R8GMSCoreFixedPointTest.java
deleted file mode 100644
index 67979ba..0000000
--- a/src/test/java/com/android/tools/r8/internal/R8GMSCoreFixedPointTest.java
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright (c) 2016, 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.internal;
-
-import com.android.tools.r8.CompilationMode;
-import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.utils.AndroidApiLevel;
-import com.android.tools.r8.utils.AndroidApp;
-import com.android.tools.r8.utils.InternalOptions;
-import java.nio.file.Paths;
-import org.junit.Test;
-
-public class R8GMSCoreFixedPointTest extends GMSCoreCompilationTestBase {
-
-  @Test
-  public void fixedPoint() throws Exception {
-    // First compilation.
-    AndroidApp app = ToolHelper.builderFromProgramDirectory(Paths.get(GMSCORE_V7_DIR)).build();
-
-    AndroidApp app1 =
-        ToolHelper.runR8(
-            ToolHelper.prepareR8CommandBuilder(app)
-                .setMode(CompilationMode.DEBUG)
-                .setMinApiLevel(AndroidApiLevel.L.getLevel())
-                .build(),
-            this::configure);
-
-    // Second compilation.
-    // Add option --skip-outline-opt for second compilation. The second compilation can find
-    // additional outlining opportunities as member rebinding from the first compilation can move
-    // methods.
-    // See b/33410508 and b/33475705.
-    AndroidApp app2 =
-        ToolHelper.runR8(
-            ToolHelper.prepareR8CommandBuilder(app1)
-                .setMode(CompilationMode.DEBUG)
-                .setMinApiLevel(AndroidApiLevel.L.getLevel())
-                .build(),
-            this::configure);
-
-    assertIdenticalApplicationsUpToCode(app1, app2, false);
-  }
-
-  private void configure(InternalOptions options) {
-    options.ignoreMissingClasses = true;
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/internal/R8GMSCoreLatestTreeShakeJarVerificationTest.java b/src/test/java/com/android/tools/r8/internal/R8GMSCoreLatestTreeShakeJarVerificationTest.java
deleted file mode 100644
index d4e1e43..0000000
--- a/src/test/java/com/android/tools/r8/internal/R8GMSCoreLatestTreeShakeJarVerificationTest.java
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright (c) 2019, 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.internal;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-
-import com.android.tools.r8.CompilationMode;
-import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.utils.AndroidApp;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Sets;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-import org.junit.Test;
-
-public class R8GMSCoreLatestTreeShakeJarVerificationTest
-    extends R8GMSCoreTreeShakeJarVerificationTest {
-
-  private String proguardMap1 = null;
-  private String proguardMap2 = null;
-
-  @Test
-  public void buildAndTreeShakeFromDeployJar() throws Exception {
-    List<String> additionalProguardConfiguration =
-        ImmutableList.of(
-            ToolHelper.PROGUARD_SETTINGS_FOR_INTERNAL_APPS + "GmsCore_proguard.config");
-    Map<String, String> idsRoundOne = new ConcurrentHashMap<>();
-    AndroidApp app1 =
-        buildAndTreeShakeFromDeployJar(
-            CompilationMode.RELEASE,
-            GMSCORE_LATEST_DIR,
-            false,
-            GMSCORE_LATEST_MAX_SIZE,
-            additionalProguardConfiguration,
-            options -> {
-              options.testing.processingContextsConsumer =
-                  id -> assertNull(idsRoundOne.put(id, id));
-              options.proguardMapConsumer =
-                  ToolHelper.consumeString(proguardMap -> this.proguardMap1 = proguardMap);
-            });
-    Map<String, String> idsRoundTwo = new ConcurrentHashMap<>();
-    AndroidApp app2 =
-        buildAndTreeShakeFromDeployJar(
-            CompilationMode.RELEASE,
-            GMSCORE_LATEST_DIR,
-            false,
-            GMSCORE_LATEST_MAX_SIZE,
-            additionalProguardConfiguration,
-            options -> {
-              options.testing.processingContextsConsumer =
-                  id -> {
-                    assertNotNull(idsRoundOne.get(id));
-                    assertNull(idsRoundTwo.put(id, id));
-                  };
-              options.proguardMapConsumer =
-                  ToolHelper.consumeString(proguardMap -> this.proguardMap2 = proguardMap);
-            });
-
-    // Verify that the result of the two compilations was the same.
-    assertEquals(
-        Collections.emptySet(),
-        Sets.symmetricDifference(idsRoundOne.keySet(), idsRoundTwo.keySet()));
-    assertIdenticalApplications(app1, app2);
-    assertEquals(proguardMap1, proguardMap2);
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/internal/R8GMSCoreTreeShakeJarVerificationTest.java b/src/test/java/com/android/tools/r8/internal/R8GMSCoreTreeShakeJarVerificationTest.java
deleted file mode 100644
index c577d60..0000000
--- a/src/test/java/com/android/tools/r8/internal/R8GMSCoreTreeShakeJarVerificationTest.java
+++ /dev/null
@@ -1,54 +0,0 @@
-// Copyright (c) 2016, 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.internal;
-
-import static org.junit.Assert.assertTrue;
-
-import com.android.tools.r8.CompilationMode;
-import com.android.tools.r8.R8RunArtTestsTest.CompilerUnderTest;
-import com.android.tools.r8.utils.AndroidApp;
-import com.android.tools.r8.utils.InternalOptions;
-import com.google.common.collect.ImmutableList;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.function.Consumer;
-
-public class R8GMSCoreTreeShakeJarVerificationTest extends GMSCoreCompilationTestBase {
-
-  public AndroidApp buildAndTreeShakeFromDeployJar(
-      CompilationMode mode,
-      String base,
-      boolean hasReference,
-      int maxSize,
-      Consumer<InternalOptions> optionsConsumer)
-      throws Exception {
-    return buildAndTreeShakeFromDeployJar(
-        mode, base, hasReference, maxSize, ImmutableList.of(), optionsConsumer);
-  }
-
-  public AndroidApp buildAndTreeShakeFromDeployJar(
-      CompilationMode mode,
-      String base,
-      boolean hasReference,
-      int maxSize,
-      List<String> additionalProguardConfigurations,
-      Consumer<InternalOptions> optionsConsumer)
-      throws Exception {
-    List<String> proguardConfigurations = new ArrayList<>(additionalProguardConfigurations);
-    proguardConfigurations.add(base + PG_CONF);
-    AndroidApp app =
-        runAndCheckVerification(
-            CompilerUnderTest.R8,
-            mode,
-            hasReference ? base + REFERENCE_APK : null,
-            proguardConfigurations,
-            optionsConsumer,
-            // Don't pass any inputs. The input will be read from the -injars in the Proguard
-            // configuration file.
-            ImmutableList.of());
-    int bytes = app.applicationSize();
-    assertTrue("Expected max size of " + maxSize + ", got " + bytes, bytes < maxSize);
-    return app;
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/internal/R8GMSCoreV10DeployJarVerificationTest.java b/src/test/java/com/android/tools/r8/internal/R8GMSCoreV10DeployJarVerificationTest.java
deleted file mode 100644
index 2c2adbf..0000000
--- a/src/test/java/com/android/tools/r8/internal/R8GMSCoreV10DeployJarVerificationTest.java
+++ /dev/null
@@ -1,74 +0,0 @@
-// Copyright (c) 2017, 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.internal;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-
-import com.android.tools.r8.CompilationMode;
-import com.android.tools.r8.DexIndexedConsumer.ArchiveConsumer;
-import com.android.tools.r8.R8RunArtTestsTest.CompilerUnderTest;
-import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.utils.AndroidApp;
-import com.google.common.collect.Sets;
-import java.io.File;
-import java.util.Collections;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-import org.junit.Test;
-
-public class R8GMSCoreV10DeployJarVerificationTest extends GMSCoreDeployJarVerificationTest {
-
-  private String proguardMap1 = null;
-  private String proguardMap2 = null;
-
-  @Test
-  public void buildFromDeployJar() throws Exception {
-    File tempFolder = temp.newFolder();
-    File app1Zip = new File(tempFolder, "app1.zip");
-    Map<String, String> idsRoundOne = new ConcurrentHashMap<>();
-    AndroidApp app1 =
-        buildFromDeployJar(
-            CompilerUnderTest.R8,
-            CompilationMode.RELEASE,
-            GMSCoreCompilationTestBase.GMSCORE_V10_DIR,
-            false,
-            options -> {
-              options.testing.processingContextsConsumer =
-                  id -> assertNull(idsRoundOne.put(id, id));
-              options.proguardMapConsumer =
-                  ToolHelper.consumeString(proguardMap -> this.proguardMap1 = proguardMap);
-            },
-            () -> new ArchiveConsumer(app1Zip.toPath(), true));
-
-    File app2Zip = new File(tempFolder, "app2.zip");
-    Map<String, String> idsRoundTwo = new ConcurrentHashMap<>();
-    AndroidApp app2 =
-        buildFromDeployJar(
-            CompilerUnderTest.R8,
-            CompilationMode.RELEASE,
-            GMSCoreCompilationTestBase.GMSCORE_V10_DIR,
-            false,
-            options -> {
-              options.testing.processingContextsConsumer =
-                  id -> {
-                    assertNotNull(idsRoundOne.get(id));
-                    assertNull(idsRoundTwo.put(id, id));
-                  };
-              options.proguardMapConsumer =
-                  ToolHelper.consumeString(proguardMap -> this.proguardMap2 = proguardMap);
-            },
-            () -> new ArchiveConsumer(app2Zip.toPath(), true));
-
-    // Verify that the result of the two compilations was the same.
-    assertEquals(
-        Collections.emptySet(),
-        Sets.symmetricDifference(idsRoundOne.keySet(), idsRoundTwo.keySet()));
-    assertIdenticalApplications(app1, app2);
-    assertIdenticalZipFiles(app1Zip, app2Zip);
-    assertEquals(proguardMap1, proguardMap2);
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/internal/R8GMSCoreV10JumboStringTest.java b/src/test/java/com/android/tools/r8/internal/R8GMSCoreV10JumboStringTest.java
deleted file mode 100644
index 365dc58..0000000
--- a/src/test/java/com/android/tools/r8/internal/R8GMSCoreV10JumboStringTest.java
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright (c) 2019, 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.internal;
-
-import com.android.tools.r8.CompilationMode;
-import org.junit.Test;
-
-public class R8GMSCoreV10JumboStringTest extends R8GMSCoreTreeShakeJarVerificationTest {
-
-  @Test
-  public void verify() throws Exception {
-    buildAndTreeShakeFromDeployJar(
-        CompilationMode.RELEASE,
-        GMSCORE_V10_DIR,
-        false,
-        GMSCORE_V10_MAX_SIZE,
-        options -> options.testing.forceJumboStringProcessing = true);
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/internal/R8GMSCoreV10TreeShakeJarVerificationTest.java b/src/test/java/com/android/tools/r8/internal/R8GMSCoreV10TreeShakeJarVerificationTest.java
deleted file mode 100644
index dc78be2..0000000
--- a/src/test/java/com/android/tools/r8/internal/R8GMSCoreV10TreeShakeJarVerificationTest.java
+++ /dev/null
@@ -1,64 +0,0 @@
-// Copyright (c) 2017, 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.internal;
-
-import static com.android.tools.r8.utils.AssertionUtils.assertNotNull;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
-
-import com.android.tools.r8.CompilationMode;
-import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.utils.AndroidApp;
-import com.google.common.collect.Sets;
-import java.util.Collections;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-import org.junit.Test;
-
-public class R8GMSCoreV10TreeShakeJarVerificationTest
-    extends R8GMSCoreTreeShakeJarVerificationTest {
-
-  private String proguardMap1 = null;
-  private String proguardMap2 = null;
-
-  @Test
-  public void buildAndTreeShakeFromDeployJar() throws Exception {
-    Map<String, String> idsRoundOne = new ConcurrentHashMap<>();
-    AndroidApp app1 =
-        buildAndTreeShakeFromDeployJar(
-            CompilationMode.RELEASE,
-            GMSCORE_V10_DIR,
-            false,
-            GMSCORE_V10_MAX_SIZE,
-            options -> {
-              options.testing.processingContextsConsumer =
-                  id -> assertNull(idsRoundOne.put(id, id));
-              options.proguardMapConsumer =
-                  ToolHelper.consumeString(proguardMap -> this.proguardMap1 = proguardMap);
-            });
-    Map<String, String> idsRoundTwo = new ConcurrentHashMap<>();
-    AndroidApp app2 =
-        buildAndTreeShakeFromDeployJar(
-            CompilationMode.RELEASE,
-            GMSCORE_V10_DIR,
-            false,
-            GMSCORE_V10_MAX_SIZE,
-            options -> {
-              options.testing.processingContextsConsumer =
-                  id -> {
-                    assertNotNull(idsRoundOne.get(id));
-                    assertNull(idsRoundTwo.put(id, id));
-                  };
-              options.proguardMapConsumer =
-                  ToolHelper.consumeString(proguardMap -> this.proguardMap2 = proguardMap);
-            });
-
-    // Verify that the result of the two compilations was the same.
-    assertEquals(
-        Collections.emptySet(),
-        Sets.symmetricDifference(idsRoundOne.keySet(), idsRoundTwo.keySet()));
-    assertIdenticalApplications(app1, app2);
-    assertEquals(proguardMap1, proguardMap2);
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/internal/R8GMSCoreV4VerificationTest.java b/src/test/java/com/android/tools/r8/internal/R8GMSCoreV4VerificationTest.java
deleted file mode 100644
index 1b650e5..0000000
--- a/src/test/java/com/android/tools/r8/internal/R8GMSCoreV4VerificationTest.java
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright (c) 2016, 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.internal;
-
-import com.android.tools.r8.CompilationMode;
-import org.junit.Test;
-
-public class R8GMSCoreV4VerificationTest extends GMSCoreCompilationTestBase {
-  @Test
-  public void verify() throws Exception {
-    runR8AndCheckVerification(CompilationMode.RELEASE, GMSCORE_V4_DIR);
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/internal/R8GMSCoreV5VerificationTest.java b/src/test/java/com/android/tools/r8/internal/R8GMSCoreV5VerificationTest.java
deleted file mode 100644
index df73644..0000000
--- a/src/test/java/com/android/tools/r8/internal/R8GMSCoreV5VerificationTest.java
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright (c) 2016, 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.internal;
-
-import com.android.tools.r8.CompilationMode;
-import org.junit.Test;
-
-public class R8GMSCoreV5VerificationTest extends GMSCoreCompilationTestBase {
-  @Test
-  public void verify() throws Exception {
-    runR8AndCheckVerification(CompilationMode.RELEASE, GMSCORE_V5_DIR);
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/internal/R8GMSCoreV6VerificationTest.java b/src/test/java/com/android/tools/r8/internal/R8GMSCoreV6VerificationTest.java
deleted file mode 100644
index a872ff5..0000000
--- a/src/test/java/com/android/tools/r8/internal/R8GMSCoreV6VerificationTest.java
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright (c) 2016, 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.internal;
-
-import com.android.tools.r8.CompilationMode;
-import org.junit.Test;
-
-public class R8GMSCoreV6VerificationTest extends GMSCoreCompilationTestBase {
-  @Test
-  public void verify() throws Exception {
-    runR8AndCheckVerification(CompilationMode.RELEASE, GMSCORE_V6_DIR);
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/internal/R8GMSCoreV7VerificationTest.java b/src/test/java/com/android/tools/r8/internal/R8GMSCoreV7VerificationTest.java
deleted file mode 100644
index 2cda255..0000000
--- a/src/test/java/com/android/tools/r8/internal/R8GMSCoreV7VerificationTest.java
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright (c) 2016, 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.internal;
-
-import com.android.tools.r8.CompilationMode;
-import org.junit.Test;
-
-public class R8GMSCoreV7VerificationTest extends GMSCoreCompilationTestBase {
-
-  @Test
-  public void verify() throws Exception {
-    runR8AndCheckVerification(CompilationMode.RELEASE, GMSCORE_V7_DIR);
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/internal/R8GMSCoreV8VerificationTest.java b/src/test/java/com/android/tools/r8/internal/R8GMSCoreV8VerificationTest.java
deleted file mode 100644
index 3f48336..0000000
--- a/src/test/java/com/android/tools/r8/internal/R8GMSCoreV8VerificationTest.java
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright (c) 2016, 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.internal;
-
-import com.android.tools.r8.CompilationMode;
-import org.junit.Test;
-
-public class R8GMSCoreV8VerificationTest extends GMSCoreCompilationTestBase {
-
-  @Test
-  public void verify() throws Exception {
-    runR8AndCheckVerification(CompilationMode.RELEASE, GMSCORE_V8_DIR);
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/internal/R8GMSCoreV9DeployJarVerificationTest.java b/src/test/java/com/android/tools/r8/internal/R8GMSCoreV9DeployJarVerificationTest.java
deleted file mode 100644
index fb06e84..0000000
--- a/src/test/java/com/android/tools/r8/internal/R8GMSCoreV9DeployJarVerificationTest.java
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright (c) 2017, 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.internal;
-
-import com.android.tools.r8.CompilationMode;
-import com.android.tools.r8.R8RunArtTestsTest.CompilerUnderTest;
-import org.junit.Test;
-
-public class R8GMSCoreV9DeployJarVerificationTest extends GMSCoreDeployJarVerificationTest {
-
-  @Test
-  public void buildFromDeployJar() throws Exception {
-    buildFromDeployJar(
-        CompilerUnderTest.R8, CompilationMode.RELEASE,
-        GMSCoreCompilationTestBase.GMSCORE_V9_DIR, true);
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/internal/R8GMSCoreV9TreeShakeJarVerificationTest.java b/src/test/java/com/android/tools/r8/internal/R8GMSCoreV9TreeShakeJarVerificationTest.java
deleted file mode 100644
index a9e28c9..0000000
--- a/src/test/java/com/android/tools/r8/internal/R8GMSCoreV9TreeShakeJarVerificationTest.java
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright (c) 2017, 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.internal;
-
-import com.android.tools.r8.CompilationMode;
-import com.android.tools.r8.StringConsumer.FileConsumer;
-import java.io.File;
-import java.nio.file.Path;
-import org.junit.Test;
-
-public class R8GMSCoreV9TreeShakeJarVerificationTest extends R8GMSCoreTreeShakeJarVerificationTest {
-
-  @Test
-  public void buildAndTreeShakeFromDeployJar() throws Exception {
-    Path proguardMapPath = File.createTempFile("mapping", ".txt", temp.getRoot()).toPath();
-    buildAndTreeShakeFromDeployJar(
-        CompilationMode.RELEASE,
-        GMSCORE_V9_DIR,
-        true,
-        GMSCORE_V9_MAX_SIZE,
-        options -> options.proguardMapConsumer = new FileConsumer(proguardMapPath));
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentClassSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentClassSubject.java
index 0e2de72f..402d6b3 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentClassSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentClassSubject.java
@@ -5,7 +5,7 @@
 package com.android.tools.r8.utils.codeinspector;
 
 import com.android.tools.r8.errors.Unreachable;
-import com.android.tools.r8.graph.AccessFlags;
+import com.android.tools.r8.graph.ClassAccessFlags;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.naming.ClassNamingForNameMapper;
@@ -115,7 +115,7 @@
   }
 
   @Override
-  public AccessFlags<?> getAccessFlags() {
+  public ClassAccessFlags getAccessFlags() {
     throw new Unreachable("Absent class has no access flags");
   }
 
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/ClassSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/ClassSubject.java
index f614059..0a4f65a 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/ClassSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/ClassSubject.java
@@ -6,6 +6,7 @@
 
 import static com.android.tools.r8.ir.desugar.itf.InterfaceMethodRewriter.COMPANION_CLASS_NAME_SUFFIX;
 
+import com.android.tools.r8.graph.ClassAccessFlags;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.naming.ClassNamingForNameMapper;
@@ -167,6 +168,9 @@
     return null;
   }
 
+  @Override
+  public abstract ClassAccessFlags getAccessFlags();
+
   public abstract boolean isAbstract();
 
   public abstract boolean isAnnotation();
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/FoundClassSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/FoundClassSubject.java
index 859f58f..4ac4f95 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/FoundClassSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/FoundClassSubject.java
@@ -11,7 +11,7 @@
 import com.android.tools.r8.TestRuntime.CfRuntime;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.ProcessResult;
-import com.android.tools.r8.graph.AccessFlags;
+import com.android.tools.r8.graph.ClassAccessFlags;
 import com.android.tools.r8.graph.DexAnnotation;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexEncodedField;
@@ -356,7 +356,7 @@
   }
 
   @Override
-  public AccessFlags<?> getAccessFlags() {
+  public ClassAccessFlags getAccessFlags() {
     return getDexProgramClass().getAccessFlags();
   }
 
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/Matchers.java b/src/test/java/com/android/tools/r8/utils/codeinspector/Matchers.java
index 5083d42..3e70473 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/Matchers.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/Matchers.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.utils.codeinspector;
 
 import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
 
 import com.android.tools.r8.Collectors;
 import com.android.tools.r8.errors.Unreachable;
@@ -101,6 +102,28 @@
     };
   }
 
+  public static Matcher<ClassSubject> isInterface() {
+    return new TypeSafeMatcher<ClassSubject>() {
+      @Override
+      public boolean matchesSafely(ClassSubject subject) {
+        return subject.isPresent() && subject.getAccessFlags().isInterface();
+      }
+
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("interface");
+      }
+
+      @Override
+      public void describeMismatchSafely(ClassSubject subject, Description description) {
+        description
+            .appendText(type(subject) + " ")
+            .appendValue(name(subject))
+            .appendText(" was not");
+      }
+    };
+  }
+
   public static Matcher<Subject> isAbsent() {
     return not(isPresent());
   }
@@ -499,6 +522,22 @@
     };
   }
 
+  public static Matcher<ClassSubject> isImplementing(ClassSubject interfaceSubject) {
+    assertThat(interfaceSubject, isPresent());
+    assertThat(interfaceSubject, isInterface());
+    return new TypeSafeMatcher<ClassSubject>() {
+      @Override
+      public boolean matchesSafely(ClassSubject subject) {
+        return subject.isPresent() && subject.isImplementing(interfaceSubject);
+      }
+
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("implements ").appendText(interfaceSubject.getOriginalName());
+      }
+    };
+  }
+
   public static Matcher<RetraceFrameResult> isInlineFrame() {
     return new TypeSafeMatcher<RetraceFrameResult>() {
       @Override
diff --git a/third_party/gmscore/gmscore_v9.tar.gz.sha1 b/third_party/gmscore/gmscore_v9.tar.gz.sha1
deleted file mode 100644
index 5983b5b..0000000
--- a/third_party/gmscore/gmscore_v9.tar.gz.sha1
+++ /dev/null
@@ -1 +0,0 @@
-0066065faeb293c5a850d3319f2cb8a48d1e760d
\ No newline at end of file
diff --git a/third_party/gmscore/v4.tar.gz.sha1 b/third_party/gmscore/v4.tar.gz.sha1
deleted file mode 100644
index e4747ae..0000000
--- a/third_party/gmscore/v4.tar.gz.sha1
+++ /dev/null
@@ -1 +0,0 @@
-2330b38e52fc7cfe78c6aa94ae497a0b1a42b9a2
\ No newline at end of file
diff --git a/third_party/gmscore/v5.tar.gz.sha1 b/third_party/gmscore/v5.tar.gz.sha1
deleted file mode 100644
index 8ca5043..0000000
--- a/third_party/gmscore/v5.tar.gz.sha1
+++ /dev/null
@@ -1 +0,0 @@
-c96d3360ba55718eca7d9c7ac9edb801a2ed4cc5
\ No newline at end of file
diff --git a/third_party/gmscore/v6.tar.gz.sha1 b/third_party/gmscore/v6.tar.gz.sha1
deleted file mode 100644
index 6cca80b..0000000
--- a/third_party/gmscore/v6.tar.gz.sha1
+++ /dev/null
@@ -1 +0,0 @@
-05eaeaa62842453108ca7191595801bb52e24861
\ No newline at end of file
diff --git a/third_party/gmscore/v7.tar.gz.sha1 b/third_party/gmscore/v7.tar.gz.sha1
deleted file mode 100644
index 502e1cf..0000000
--- a/third_party/gmscore/v7.tar.gz.sha1
+++ /dev/null
@@ -1 +0,0 @@
-01da4f538ff748867d7fa9a7a9ba262d8aa62922
\ No newline at end of file
diff --git a/third_party/gmscore/v8.tar.gz.sha1 b/third_party/gmscore/v8.tar.gz.sha1
deleted file mode 100644
index 9eab429..0000000
--- a/third_party/gmscore/v8.tar.gz.sha1
+++ /dev/null
@@ -1 +0,0 @@
-ee245a82b2470a1c60ac7fa085d2b5ce2101a3da
\ No newline at end of file
diff --git a/tools/compiledump.py b/tools/compiledump.py
index b508c6f..cc71d22 100755
--- a/tools/compiledump.py
+++ b/tools/compiledump.py
@@ -176,11 +176,10 @@
     return Dump(args.dump)
   dump_file = zipfile.ZipFile(os.path.abspath(args.dump), 'r')
   with utils.ChangedWorkingDirectory(temp):
-    if args.override or not os.path.isfile(
-        os.path.join(temp, 'r8-version')):
+    if args.override or not os.path.isfile('r8-version'):
       print("Extracting into: %s" % temp)
       dump_file.extractall()
-      if not os.path.isfile(os.path.join(temp, 'r8-version')):
+      if not os.path.isfile('r8-version'):
         error("Did not extract into %s. Either the zip file is invalid or the "
               "dump is missing files" % temp)
     return Dump(temp)
diff --git a/tools/gmscore_data.py b/tools/gmscore_data.py
index bef5ba3..95ee0bb 100644
--- a/tools/gmscore_data.py
+++ b/tools/gmscore_data.py
@@ -6,89 +6,18 @@
 import os
 import utils
 
+ANDROID_L_API = '21'
 BASE = os.path.join(utils.THIRD_PARTY, 'gmscore')
 
-V4_BASE = os.path.join(BASE, 'v4')
-V5_BASE = os.path.join(BASE, 'v5')
-V6_BASE = os.path.join(BASE, 'v6')
-V7_BASE = os.path.join(BASE, 'v7')
-V8_BASE = os.path.join(BASE, 'v8')
-
-V9_BASE = os.path.join(BASE, 'gmscore_v9')
-V9_PREFIX = os.path.join(V9_BASE, 'GmsCore_prod_alldpi_release_all_locales')
-
 V10_BASE = os.path.join(BASE, 'gmscore_v10')
 V10_PREFIX = os.path.join(V10_BASE, 'GmsCore_prod_alldpi_release_all_locales')
 
 LATEST_BASE = os.path.join(BASE, 'latest')
 LATEST_PREFIX = os.path.join(LATEST_BASE, 'GmsCore_prod_alldpi_release_all_locales')
-ANDROID_L_API = '21'
 
-# NOTE: we always use android.jar for SDK v25, later we might want to revise it
-#       to use proper android.jar version for each of gmscore version separately.
-ANDROID_JAR = utils.get_android_jar(25)
+LATEST_VERSION = 'latest'
 
 VERSIONS = {
-  'v4': {
-    'dex' : {
-      'inputs' : glob.glob(os.path.join(V4_BASE, '*.dex')),
-      'pgmap' : os.path.join(V4_BASE, 'proguard.map'),
-      'libraries' : [ANDROID_JAR],
-      'min-api' : ANDROID_L_API,
-    }
-  },
-  'v5': {
-    'dex' : {
-      'inputs' : glob.glob(os.path.join(V5_BASE, '*.dex')),
-      'pgmap' : os.path.join(V5_BASE, 'proguard.map'),
-      'libraries' : [ANDROID_JAR],
-      'min-api' : ANDROID_L_API,
-    }
-  },
-  'v6': {
-    'dex' : {
-      'inputs' : glob.glob(os.path.join(V6_BASE, '*.dex')),
-      'pgmap' : os.path.join(V6_BASE, 'proguard.map'),
-      'libraries' : [ANDROID_JAR],
-      'min-api' : ANDROID_L_API,
-    }
-  },
-  'v7': {
-    'dex' : {
-      'inputs' : glob.glob(os.path.join(V7_BASE, '*.dex')),
-      'pgmap' : os.path.join(V7_BASE, 'proguard.map'),
-      'libraries' : [ANDROID_JAR],
-      'min-api' : ANDROID_L_API,
-    }
-  },
-  'v8': {
-    'dex' : {
-      'inputs' : glob.glob(os.path.join(V8_BASE, '*.dex')),
-      'pgmap' : os.path.join(V8_BASE, 'proguard.map'),
-      'libraries' : [ANDROID_JAR],
-      'min-api' : ANDROID_L_API,
-    }
-  },
-  'v9': {
-    'dex' : {
-      'flags': '--no-desugaring',
-      'inputs': [os.path.join(V9_BASE, 'armv7_GmsCore_prod_alldpi_release.apk')],
-      'main-dex-list': os.path.join(V9_BASE, 'main_dex_list.txt'),
-      'pgmap': '%s_proguard.map' % V9_PREFIX,
-    },
-    'deploy' : {
-      'pgconf': ['%s_proguard.config' % V9_PREFIX,
-                 utils.IGNORE_WARNINGS_RULES],
-      'inputs': ['%s_deploy.jar' % V9_PREFIX],
-      'min-api' : ANDROID_L_API,
-    },
-    'proguarded' : {
-      'flags': '--no-desugaring',
-      'inputs': ['%s_proguard.jar' % V9_PREFIX],
-      'main-dex-list': os.path.join(V9_BASE, 'main_dex_list.txt'),
-      'pgmap': '%s_proguard.map' % V9_PREFIX,
-     }
-  },
   'v10': {
     'dex' : {
       'flags': '--no-desugaring',
diff --git a/tools/r8_release.py b/tools/r8_release.py
index 397c97d..7914bf9 100755
--- a/tools/r8_release.py
+++ b/tools/r8_release.py
@@ -15,7 +15,7 @@
 
 import utils
 
-R8_DEV_BRANCH = '3.0'
+R8_DEV_BRANCH = '3.1'
 R8_VERSION_FILE = os.path.join(
     'src', 'main', 'java', 'com', 'android', 'tools', 'r8', 'Version.java')
 THIS_FILE_RELATIVE = os.path.join('tools', 'r8_release.py')
@@ -126,7 +126,7 @@
 
 def version_change_diff(diff, old_version, new_version):
   invalid_line = None
-  for line in diff.splitlines():
+  for line in str(diff).splitlines():
     if line.startswith('-  ') and \
         line != '-  public static final String LABEL = "%s";' % old_version:
       invalid_line = line
@@ -634,7 +634,7 @@
 
 def branch_change_diff(diff, old_version, new_version):
   invalid_line = None
-  for line in diff.splitlines():
+  for line in str(diff).splitlines():
     if line.startswith('-R8') and \
         line != "-R8_DEV_BRANCH = '%s'" % old_version:
       print(line)
diff --git a/tools/run_on_app.py b/tools/run_on_app.py
index c775aa0..cdceb6b 100755
--- a/tools/run_on_app.py
+++ b/tools/run_on_app.py
@@ -390,7 +390,7 @@
 
 def get_version_and_data(options):
   if options.app == 'gmscore':
-    version = options.version or 'v9'
+    version = options.version or gmscore_data.LATEST_VERSION
     data = gmscore_data
   elif options.app == 'nest':
     version = options.version or '20180926'