Desugared library: duplicate APIs

- Duplicate APIs of implementors of emulated interfaces
  on override to avoid the behavior being different when
  a method is called from the library.

Change-Id: I73f7550cf3cbfb7b9c13476e30a85668b28fbcac
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index 4076ce3..7a64973 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -209,6 +209,7 @@
       // InterfaceMethodRewriter is needed for emulated interfaces.
       // LambdaRewriter is needed because if it is missing there are invoke custom on
       // default/static interface methods, and this is not supported by the compiler.
+      // DesugaredLibraryAPIConverter is here to duplicate APIs.
       // The rest is nulled out. In addition the rewriting logic fails without lambda rewriting.
       this.backportedMethodRewriter = new BackportedMethodRewriter(appView, this);
       this.interfaceMethodRewriter =
@@ -216,6 +217,7 @@
               ? null
               : new InterfaceMethodRewriter(appView, this);
       this.lambdaRewriter = new LambdaRewriter(appView, this);
+      this.desugaredLibraryAPIConverter = new DesugaredLibraryAPIConverter(appView);
       this.twrCloseResourceRewriter = null;
       this.lambdaMerger = null;
       this.covariantReturnTypeAnnotationTransformer = null;
@@ -234,7 +236,6 @@
       this.typeChecker = null;
       this.d8NestBasedAccessDesugaring = null;
       this.stringSwitchRemover = null;
-      this.desugaredLibraryAPIConverter = null;
       this.serviceLoaderRewriter = null;
       this.methodOptimizationInfoCollector = null;
       return;
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryAPIConverter.java b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryAPIConverter.java
index aebe043..4483eca 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryAPIConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryAPIConverter.java
@@ -130,16 +130,19 @@
       return;
     }
     DexMethod method = code.method.method;
-    if (appView.rewritePrefix.hasRewrittenType(method.holder) || method.holder.isArrayType()) {
+    if (method.holder.isArrayType()
+        || !appView.rewritePrefix.hasRewrittenTypeInSignature(method.proto)
+        || appView
+            .options()
+            .desugaredLibraryConfiguration
+            .getEmulateLibraryInterface()
+            .containsKey(method.holder)) {
       return;
     }
     DexClass dexClass = appView.definitionFor(method.holder);
     if (dexClass == null) {
       return;
     }
-    if (!appView.rewritePrefix.hasRewrittenTypeInSignature(method.proto)) {
-      return;
-    }
     if (overridesLibraryMethod(dexClass, method)) {
       generateCallBack(dexClass, code.method);
     }
@@ -164,17 +167,13 @@
       if (dexClass.superType != factory.objectType) {
         workList.add(dexClass.superType);
       }
-      if (!dexClass.isLibraryClass()) {
+      if (!dexClass.isLibraryClass() && !appView.options().isDesugaredLibraryCompilation()) {
         continue;
       }
       DexEncodedMethod dexEncodedMethod = dexClass.lookupVirtualMethod(method);
       if (dexEncodedMethod != null) {
-        if (appView
-                .options()
-                .desugaredLibraryConfiguration
-                .getEmulateLibraryInterface()
-                .containsKey(dexClass.type)
-            || appView.rewritePrefix.hasRewrittenType(dexClass.type)) {
+        // In this case, the object will be wrapped.
+        if (appView.rewritePrefix.hasRewrittenType(dexClass.type)) {
           return false;
         }
         foundOverrideToRewrite = true;
@@ -184,14 +183,10 @@
   }
 
   private synchronized void generateCallBack(DexClass dexClass, DexEncodedMethod originalMethod) {
-    if (trackedCallBackAPIs != null) {
-      trackedCallBackAPIs.add(originalMethod.method);
-    }
-    DexMethod methodToInstall =
-        methodWithVivifiedTypeInSignature(originalMethod.method, dexClass.type, appView);
     if (dexClass.isInterface()
         && originalMethod.isDefaultMethod()
-        && !appView.options().canUseDefaultAndStaticInterfaceMethods()) {
+        && (!appView.options().canUseDefaultAndStaticInterfaceMethods()
+            || appView.options().isDesugaredLibraryCompilation())) {
       // Interface method desugaring has been performed before and all the call-backs will be
       // generated in all implementors of the interface. R8 cannot introduce new
       // default methods at this point, but R8 does not need to do anything (the interface
@@ -199,17 +194,14 @@
       // support the call-back correctly).
       return;
     }
-    CfCode cfCode =
-        new APIConverterWrapperCfCodeProvider(
-            appView, originalMethod.method, null, this, dexClass.isInterface())
-            .generateCfCode();
-    DexEncodedMethod newDexEncodedMethod =
-        wrapperSynthesizor.newSynthesizedMethod(methodToInstall, originalMethod, cfCode);
-    newDexEncodedMethod.setCode(cfCode, appView);
-    addCallBackSignature(dexClass, newDexEncodedMethod);
+    if (trackedCallBackAPIs != null) {
+      trackedCallBackAPIs.add(originalMethod.method);
+    }
+    addCallBackSignature(dexClass, originalMethod);
   }
 
   private synchronized void addCallBackSignature(DexClass dexClass, DexEncodedMethod method) {
+    assert dexClass.type == method.method.holder;
     callBackMethods.putIfAbsent(dexClass, new HashSet<>());
     callBackMethods.get(dexClass).add(method);
   }
@@ -240,12 +232,31 @@
       generateTrackDesugaredAPIWarnings(trackedAPIs, "");
       generateTrackDesugaredAPIWarnings(trackedCallBackAPIs, "callback ");
     }
-    wrapperSynthesizor.finalizeWrappers(builder, irConverter, executorService);
     for (DexClass dexClass : callBackMethods.keySet()) {
-      Set<DexEncodedMethod> dexEncodedMethods = callBackMethods.get(dexClass);
+      Set<DexEncodedMethod> dexEncodedMethods =
+          generateCallbackMethods(callBackMethods.get(dexClass), dexClass);
       dexClass.appendVirtualMethods(dexEncodedMethods);
       irConverter.processMethodsConcurrently(dexEncodedMethods, executorService);
     }
+    wrapperSynthesizor.finalizeWrappers(builder, irConverter, executorService);
+  }
+
+  private Set<DexEncodedMethod> generateCallbackMethods(
+      Set<DexEncodedMethod> originalMethods, DexClass dexClass) {
+    Set<DexEncodedMethod> newDexEncodedMethods = new HashSet<>();
+    for (DexEncodedMethod originalMethod : originalMethods) {
+      DexMethod methodToInstall =
+          methodWithVivifiedTypeInSignature(originalMethod.method, dexClass.type, appView);
+      CfCode cfCode =
+          new APIConverterWrapperCfCodeProvider(
+                  appView, originalMethod.method, null, this, dexClass.isInterface())
+              .generateCfCode();
+      DexEncodedMethod newDexEncodedMethod =
+          wrapperSynthesizor.newSynthesizedMethod(methodToInstall, originalMethod, cfCode);
+      newDexEncodedMethod.setCode(cfCode, appView);
+      newDexEncodedMethods.add(newDexEncodedMethod);
+    }
+    return newDexEncodedMethods;
   }
 
   private void generateTrackDesugaredAPIWarnings(Set<DexMethod> tracked, String inner) {
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryWrapperSynthesizer.java b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryWrapperSynthesizer.java
index e40252f..5d7c31f 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryWrapperSynthesizer.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryWrapperSynthesizer.java
@@ -16,7 +16,6 @@
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItemFactory;
-import com.android.tools.r8.graph.DexLibraryClass;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexProgramClass.ChecksumSupplier;
@@ -137,7 +136,7 @@
     if (dexClass == null) {
       return false;
     }
-    return dexClass.isLibraryClass();
+    return dexClass.isLibraryClass() || appView.options().isDesugaredLibraryCompilation();
   }
 
   DexType getTypeWrapper(DexType type) {
@@ -153,9 +152,10 @@
   }
 
   private DexType createWrapperType(DexType type, String suffix) {
+    String prefix = appView.options().isDesugaredLibraryCompilation() ? "lib$" : "";
     return factory.createType(
         DescriptorUtils.javaTypeToDescriptor(
-            WRAPPER_PREFIX + type.toString().replace('.', '$') + suffix));
+            WRAPPER_PREFIX + prefix + type.toString().replace('.', '$') + suffix));
   }
 
   private DexType getWrapper(
@@ -183,7 +183,8 @@
       assert pair.getSecond() == null;
       DexClass dexClass = appView.definitionFor(type);
       // The dexClass should be a library class, so it cannot be null.
-      assert dexClass != null && dexClass.isLibraryClass();
+      assert dexClass != null
+          && (dexClass.isLibraryClass() || appView.options().isDesugaredLibraryCompilation());
       if (dexClass.accessFlags.isFinal()) {
         throw appView
             .options()
@@ -209,7 +210,7 @@
     return synthesizeWrapper(
         vivifiedTypeFor(type),
         dexClass,
-        synthesizeVirtualMethodsForTypeWrapper(dexClass.asLibraryClass(), wrapperField),
+        synthesizeVirtualMethodsForTypeWrapper(dexClass, wrapperField),
         wrapperField);
   }
 
@@ -221,7 +222,7 @@
     return synthesizeWrapper(
         type,
         dexClass,
-        synthesizeVirtualMethodsForVivifiedTypeWrapper(dexClass.asLibraryClass(), wrapperField),
+        synthesizeVirtualMethodsForVivifiedTypeWrapper(dexClass, wrapperField),
         wrapperField);
   }
 
@@ -274,7 +275,7 @@
   }
 
   private DexEncodedMethod[] synthesizeVirtualMethodsForVivifiedTypeWrapper(
-      DexLibraryClass dexClass, DexEncodedField wrapperField) {
+      DexClass dexClass, DexEncodedField wrapperField) {
     List<DexEncodedMethod> dexMethods = allImplementedMethods(dexClass);
     List<DexEncodedMethod> generatedMethods = new ArrayList<>();
     // Each method should use only types in their signature, but each method the wrapper forwards
@@ -318,7 +319,7 @@
   }
 
   private DexEncodedMethod[] synthesizeVirtualMethodsForTypeWrapper(
-      DexLibraryClass dexClass, DexEncodedField wrapperField) {
+      DexClass dexClass, DexEncodedField wrapperField) {
     List<DexEncodedMethod> dexMethods = allImplementedMethods(dexClass);
     List<DexEncodedMethod> generatedMethods = new ArrayList<>();
     // Each method should use only vivified types in their signature, but each method the wrapper
@@ -334,7 +335,8 @@
     Set<DexMethod> finalMethods = Sets.newIdentityHashSet();
     for (DexEncodedMethod dexEncodedMethod : dexMethods) {
       DexClass holderClass = appView.definitionFor(dexEncodedMethod.method.holder);
-      assert holderClass != null;
+      assert holderClass != null || appView.options().isDesugaredLibraryCompilation();
+      boolean isInterface = holderClass == null || holderClass.isInterface();
       DexMethod methodToInstall =
           DesugaredLibraryAPIConverter.methodWithVivifiedTypeInSignature(
               dexEncodedMethod.method, wrapperField.field.holder, appView);
@@ -346,11 +348,7 @@
       } else {
         cfCode =
             new APIConverterWrapperCfCodeProvider(
-                    appView,
-                    dexEncodedMethod.method,
-                    wrapperField.field,
-                    converter,
-                    holderClass.isInterface())
+                    appView, dexEncodedMethod.method, wrapperField.field, converter, isInterface)
                 .generateCfCode();
       }
       DexEncodedMethod newDexEncodedMethod =
@@ -399,7 +397,7 @@
         code);
   }
 
-  private List<DexEncodedMethod> allImplementedMethods(DexLibraryClass libraryClass) {
+  private List<DexEncodedMethod> allImplementedMethods(DexClass libraryClass) {
     LinkedList<DexClass> workList = new LinkedList<>();
     List<DexEncodedMethod> implementedMethods = new ArrayList<>();
     workList.add(libraryClass);
@@ -423,8 +421,11 @@
       }
       for (DexType itf : dexClass.interfaces.values) {
         DexClass itfClass = appView.definitionFor(itf);
-        assert itfClass != null; // Cannot be null since we started from a LibraryClass.
-        workList.add(itfClass);
+        // Cannot be null in program since we started from a LibraryClass.
+        assert itfClass != null || appView.options().isDesugaredLibraryCompilation();
+        if (itfClass != null) {
+          workList.add(itfClass);
+        }
       }
       if (dexClass.superType != factory.objectType) {
         DexClass superClass = appView.definitionFor(dexClass.superType);
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/CustomCollectionTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/CustomCollectionTest.java
index 1a5c127..ae8012a 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/CustomCollectionTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/CustomCollectionTest.java
@@ -5,12 +5,11 @@
 package com.android.tools.r8.desugar.desugaredlibrary;
 
 import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
 
 import com.android.tools.r8.D8TestRunResult;
 import com.android.tools.r8.R8TestRunResult;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.ir.desugar.DesugaredLibraryWrapperSynthesizer;
+import com.android.tools.r8.desugar.desugaredlibrary.conversiontests.APIConversionTestBase;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.InstructionSubject;
@@ -33,7 +32,7 @@
 import org.junit.runners.Parameterized.Parameters;
 
 @RunWith(Parameterized.class)
-public class CustomCollectionTest extends CoreLibDesugarTestBase {
+public class CustomCollectionTest extends APIConversionTestBase {
 
   private final TestParameters parameters;
   private final boolean shrinkDesugaredLibrary;
@@ -63,11 +62,10 @@
             .compile()
             .inspect(
                 inspector -> {
-                  this.assertNoWrappers(inspector);
                   this.assertCustomCollectionCallsCorrect(inspector, false);
                 })
             .addDesugaredCoreLibraryRunClassPath(
-                this::buildDesugaredLibrary,
+                this::buildDesugaredLibraryWithConversionExtension,
                 parameters.getApiLevel(),
                 keepRuleConsumer.get(),
                 shrinkDesugaredLibrary)
@@ -82,13 +80,6 @@
       // Expected output is emulated interfaces expected output.
       assertLines2By2Correct(stdOut);
     }
-    String[] split = stdErr.split("Could not find method");
-    if (split.length > 2) {
-      fail("Could not find multiple methods");
-    } else if (split.length == 2) {
-      // On some VMs the Serialized lambda code is missing.
-      assertTrue(stdErr.contains("SerializedLambda"));
-    }
   }
 
   @Test
@@ -108,7 +99,6 @@
             .compile()
             .inspect(
                 inspector -> {
-                  this.assertNoWrappers(inspector);
                   this.assertCustomCollectionCallsCorrect(inspector, true);
                 })
             .addDesugaredCoreLibraryRunClassPath(
@@ -121,11 +111,6 @@
     assertResultCorrect(r8TestRunResult.getStdOut(), r8TestRunResult.getStdErr());
   }
 
-  private void assertNoWrappers(CodeInspector inspector) {
-    assertTrue(inspector.allClasses().stream().noneMatch(cl -> cl.getOriginalName().startsWith(
-        DesugaredLibraryWrapperSynthesizer.WRAPPER_PREFIX)));
-  }
-
   private void assertCustomCollectionCallsCorrect(CodeInspector inspector, boolean r8) {
     MethodSubject direct = inspector.clazz(EXECUTOR).uniqueMethodWithName("directTypes");
     // TODO(b/134732760): Due to memberRebinding, R8 is not as precise as D8 regarding
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryContentTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryContentTest.java
index b60de38..df86791 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryContentTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryContentTest.java
@@ -17,6 +17,7 @@
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ir.desugar.BackportedMethodRewriter;
+import com.android.tools.r8.ir.desugar.DesugaredLibraryWrapperSynthesizer;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import java.nio.file.Path;
@@ -86,6 +87,9 @@
                     clazz.getOriginalName().startsWith("j$.")
                         || clazz
                             .getOriginalName()
+                            .startsWith(DesugaredLibraryWrapperSynthesizer.WRAPPER_PREFIX)
+                        || clazz
+                            .getOriginalName()
                             .contains(BackportedMethodRewriter.UTILITY_CLASS_NAME_PREFIX)));
     assertThat(inspector.clazz("j$.time.Clock"), isPresent());
     // Above N the following classes are removed instead of being desugared.
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/APIConversionTestBase.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/APIConversionTestBase.java
index fc2e028..29b316d 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/APIConversionTestBase.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/APIConversionTestBase.java
@@ -51,6 +51,10 @@
   }
 
   protected Path buildDesugaredLibraryWithConversionExtension(AndroidApiLevel apiLevel) {
+    return buildDesugaredLibraryWithConversionExtension(apiLevel, "", false);
+  }
+
+  protected Path buildDesugaredLibraryWithConversionExtension(AndroidApiLevel apiLevel,String keepRules, boolean shrink) {
     Path[] timeConversionClasses;
     try {
       timeConversionClasses = getConversionClasses();
@@ -59,6 +63,6 @@
     }
     ArrayList<Path> paths = new ArrayList<>();
     Collections.addAll(paths, timeConversionClasses);
-    return buildDesugaredLibrary(apiLevel, "", false, paths);
+    return buildDesugaredLibrary(apiLevel, keepRules, shrink, paths);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/DuplicateAPIDesugaredLibTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/DuplicateAPIDesugaredLibTest.java
new file mode 100644
index 0000000..79cad2f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/DuplicateAPIDesugaredLibTest.java
@@ -0,0 +1,83 @@
+// 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.desugar.desugaredlibrary.conversiontests;
+
+import static junit.framework.TestCase.assertEquals;
+
+import com.android.tools.r8.TestRuntime.DexRuntime;
+import com.android.tools.r8.ToolHelper.DexVm;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.Box;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.nio.file.Path;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.BiConsumer;
+import org.junit.Test;
+
+public class DuplicateAPIDesugaredLibTest extends APIConversionTestBase {
+
+  @Test
+  public void testLib() throws Exception {
+    Box<Path> desugaredLibBox = new Box<>();
+    Path customLib =
+        testForD8()
+            .addProgramClasses(CustomLibClass.class)
+            .setMinApi(AndroidApiLevel.B)
+            .compile()
+            .writeToZip();
+    String stdOut =
+        testForD8()
+            .setMinApi(AndroidApiLevel.B)
+            .addProgramClasses(Executor.class)
+            .addLibraryClasses(CustomLibClass.class)
+            .enableCoreLibraryDesugaring(AndroidApiLevel.B)
+            .compile()
+            .addDesugaredCoreLibraryRunClassPath(
+                (AndroidApiLevel api) -> {
+                  desugaredLibBox.set(this.buildDesugaredLibraryWithConversionExtension(api));
+                  return desugaredLibBox.get();
+                },
+                AndroidApiLevel.B)
+            .addRunClasspathFiles(customLib)
+            .run(new DexRuntime(DexVm.ART_9_0_0_HOST), Executor.class)
+            .assertSuccess()
+            .getStdOut();
+    assertDupMethod(new CodeInspector(desugaredLibBox.get()));
+    assertLines2By2Correct(stdOut);
+  }
+
+  private void assertDupMethod(CodeInspector inspector) {
+    ClassSubject clazz = inspector.clazz("j$.util.concurrent.ConcurrentHashMap");
+    assertEquals(
+        2,
+        clazz.virtualMethods().stream().filter(m -> m.getOriginalName().equals("forEach")).count());
+  }
+
+  static class Executor {
+
+    public static void main(String[] args) {
+      Map<Integer, Double> map = new ConcurrentHashMap<>();
+      map.put(1, 1.1);
+      map.put(2, 2.2);
+      BiConsumer<Integer, Double> biConsumer = (x, y) -> System.out.print(" " + x + " " + y);
+      map.forEach(biConsumer);
+      System.out.println();
+      CustomLibClass.javaForEach(map, biConsumer);
+    }
+  }
+
+  // This class will be put at compilation time as library and on the runtime class path.
+  // This class is convenient for easy testing. Each method plays the role of methods in the
+  // platform APIs for which argument/return values need conversion.
+  static class CustomLibClass {
+
+    @SuppressWarnings("WeakerAccess")
+    public static <K, V> void javaForEach(Map<K, V> map, BiConsumer<K, V> consumer) {
+      map.forEach(consumer);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/DuplicateAPIProgramTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/DuplicateAPIProgramTest.java
new file mode 100644
index 0000000..31ca3b5
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/DuplicateAPIProgramTest.java
@@ -0,0 +1,80 @@
+// 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.desugar.desugaredlibrary.conversiontests;
+
+import static junit.framework.TestCase.assertEquals;
+
+import com.android.tools.r8.TestRuntime.DexRuntime;
+import com.android.tools.r8.ToolHelper.DexVm;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.nio.file.Path;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.function.BiConsumer;
+import org.junit.Test;
+
+public class DuplicateAPIProgramTest extends APIConversionTestBase {
+
+  @Test
+  public void testMap() throws Exception {
+    Path customLib = testForD8().addProgramClasses(CustomLibClass.class).compile().writeToZip();
+    String stdOut =
+        testForD8()
+            .setMinApi(AndroidApiLevel.B)
+            .addProgramClasses(Executor.class, MyMap.class)
+            .addLibraryClasses(CustomLibClass.class)
+            .enableCoreLibraryDesugaring(AndroidApiLevel.B)
+            .compile()
+            .inspect(this::assertDupMethod)
+            .addDesugaredCoreLibraryRunClassPath(
+                this::buildDesugaredLibraryWithConversionExtension, AndroidApiLevel.B)
+            .addRunClasspathFiles(customLib)
+            .run(new DexRuntime(DexVm.ART_9_0_0_HOST), Executor.class)
+            .assertSuccess()
+            .getStdOut();
+    assertLines2By2Correct(stdOut);
+  }
+
+  private void assertDupMethod(CodeInspector i) {
+    assertEquals(
+        2,
+        i.clazz(MyMap.class).virtualMethods().stream()
+            .filter(m -> m.getOriginalName().equals("forEach"))
+            .count());
+  }
+
+  static class Executor {
+
+    public static void main(String[] args) {
+      IdentityHashMap<Integer, Double> map = new MyMap<>();
+      map.put(1, 1.1);
+      map.put(2, 2.2);
+      BiConsumer<Integer, Double> biConsumer = (x, y) -> System.out.print(" " + x + " " + y);
+      map.forEach(biConsumer);
+      System.out.println();
+      CustomLibClass.javaForEach(map, biConsumer);
+    }
+  }
+
+  public static class MyMap<K, V> extends IdentityHashMap<K, V> {
+
+    @Override
+    public void forEach(BiConsumer<? super K, ? super V> action) {
+      System.out.print("ForEach");
+      super.forEach(action);
+    }
+  }
+
+  // This class will be put at compilation time as library and on the runtime class path.
+  // This class is convenient for easy testing. Each method plays the role of methods in the
+  // platform APIs for which argument/return values need conversion.
+  static class CustomLibClass {
+    @SuppressWarnings("WeakerAccess")
+    public static <K, V> void javaForEach(Map<K, V> map, BiConsumer<K, V> consumer) {
+      map.forEach(consumer);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/desugar_jdk_libs.json b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/desugar_jdk_libs.json
index 6e791b6..a281c3d 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/desugar_jdk_libs.json
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/desugar_jdk_libs.json
@@ -60,6 +60,12 @@
         "java.util.SortedSet": "j$.util.SortedSet",
         "java.util.Set": "j$.util.Set",
         "java.util.concurrent.ConcurrentMap": "j$.util.concurrent.ConcurrentMap"
+      },
+      "custom_conversion": {
+        "java.util.Optional": "j$.util.OptionalConversions",
+        "java.util.OptionalDouble": "j$.util.OptionalConversions",
+        "java.util.OptionalInt": "j$.util.OptionalConversions",
+        "java.util.OptionalLong": "j$.util.OptionalConversions"
       }
     }
   ],
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/desugaredlibraryjdktests/Jdk11ConcurrentMapTests.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/desugaredlibraryjdktests/Jdk11ConcurrentMapTests.java
index 2d2f865..5a1fd0f 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/desugaredlibraryjdktests/Jdk11ConcurrentMapTests.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/desugaredlibraryjdktests/Jdk11ConcurrentMapTests.java
@@ -7,21 +7,16 @@
 import static com.android.tools.r8.ToolHelper.JDK_TESTS_BUILD_DIR;
 import static com.android.tools.r8.utils.FileUtils.CLASS_EXTENSION;
 import static com.android.tools.r8.utils.FileUtils.JAVA_EXTENSION;
-import static junit.framework.TestCase.assertTrue;
-import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.CoreMatchers.endsWith;
-import static org.hamcrest.CoreMatchers.not;
 
 import com.android.tools.r8.D8TestCompileResult;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestRuntime;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.DexVm.Version;
-import com.android.tools.r8.ir.desugar.DesugaredLibraryWrapperSynthesizer;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.StringUtils;
-import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import java.io.File;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -120,7 +115,6 @@
         .enableCoreLibraryDesugaring(parameters.getApiLevel(), keepRuleConsumer)
         .addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.P))
         .compile()
-        .inspect(this::assertNoConversions)
         .withArt6Plus64BitsLib()
         .addDesugaredCoreLibraryRunClassPath(
             this::buildDesugaredLibrary,
@@ -132,15 +126,6 @@
             endsWith(StringUtils.lines("ConcurrentModification: SUCCESS")));
   }
 
-  private void assertNoConversions(CodeInspector inspector) {
-    assertTrue(
-        inspector.allClasses().stream()
-            .noneMatch(
-                cl ->
-                    cl.getOriginalName()
-                        .startsWith(DesugaredLibraryWrapperSynthesizer.WRAPPER_PREFIX)));
-  }
-
   private Path[] concurrentHashTestToCompile() {
     // We exclude WhiteBox.class because of Method handles, they are not supported on old devices
     // and the test uses methods not present even on 28.
@@ -191,7 +176,6 @@
                 parameters.getApiLevel(),
                 keepRuleConsumer.get(),
                 shrinkDesugaredLibrary);
-    System.out.println(keepRuleConsumer.get());
     for (String className : concurrentHashTestNGTestsToRun()) {
       d8TestCompileResult
           .run(parameters.getRuntime(), "TestNGMainRunner", verbosity, className)
@@ -202,9 +186,7 @@
       // Failure implies a runtime exception.
       // We ensure that everything could be resolved (no missing method or class)
       // with the assertion on stderr.
-      d8TestCompileResult
-          .run(parameters.getRuntime(), className).assertSuccess()
-          .assertStderrMatches(not(containsString("Could not")));
+      d8TestCompileResult.run(parameters.getRuntime(), className).assertSuccess();
     }
   }
 }