Support complex hierarchy in desugared library

Bug: b/202188674
Change-Id: I23d6ef12ec723f50a62e8da7b79c557058a4de99
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 1a029f8..b891c89 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java
@@ -28,6 +28,7 @@
 import java.util.Collections;
 import java.util.Deque;
 import java.util.List;
+import java.util.Map.Entry;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
@@ -556,6 +557,16 @@
         .lookupMaximallySpecificTarget(clazz, method);
   }
 
+  /**
+   * Helper methods used for emulated interface resolution (not in JVM specifications). Answers the
+   * abstract interface methods that the resolution could but does not necessarily resolve into.
+   */
+  public List<Entry<DexClass, DexEncodedMethod>> getAbstractInterfaceMethods(
+      DexClass clazz, DexMethod method) {
+    return MethodResolution.createLegacy(this::definitionFor, dexItemFactory())
+        .getAbstractInterfaceMethods(clazz, method);
+  }
+
   MethodResolutionResult resolveMaximallySpecificTarget(DexClass clazz, DexMethod method) {
     return MethodResolution.createLegacy(this::definitionFor, dexItemFactory())
         .resolveMaximallySpecificTarget(clazz, method);
diff --git a/src/main/java/com/android/tools/r8/graph/MethodResolution.java b/src/main/java/com/android/tools/r8/graph/MethodResolution.java
index f7271b7..11f9bd9 100644
--- a/src/main/java/com/android/tools/r8/graph/MethodResolution.java
+++ b/src/main/java/com/android/tools/r8/graph/MethodResolution.java
@@ -241,6 +241,12 @@
     return resolveMaximallySpecificTargetHelper(clazz, method).lookup();
   }
 
+  // Non-private method used for emulated interface only.
+  List<Entry<DexClass, DexEncodedMethod>> getAbstractInterfaceMethods(
+      DexClass clazz, DexMethod method) {
+    return resolveMaximallySpecificTargetHelper(clazz, method).getAbstractMethods();
+  }
+
   private MaximallySpecificMethodsBuilder resolveMaximallySpecificTargetHelper(
       DexClass clazz, DexMethod method) {
     MaximallySpecificMethodsBuilder builder =
@@ -706,6 +712,28 @@
       return nonAbstractMethods;
     }
 
+    List<Entry<DexClass, DexEncodedMethod>> getAbstractMethods() {
+      List<Entry<DexClass, DexEncodedMethod>> abstractMethods = new ArrayList<>();
+      addAbstractMethods(abstractMethods, maximallySpecificMethodsOnCompletePaths);
+      addAbstractMethods(abstractMethods, maximallySpecificMethodsOnIncompletePaths);
+      return abstractMethods;
+    }
+
+    private void addAbstractMethods(
+        List<Entry<DexClass, DexEncodedMethod>> abstractMethods,
+        Map<DexClass, DexEncodedMethod> candidates) {
+      for (Entry<DexClass, DexEncodedMethod> entry : candidates.entrySet()) {
+        DexEncodedMethod method = entry.getValue();
+        if (method == null) {
+          // Ignore shadowed candidates.
+          continue;
+        }
+        if (method.isAbstract()) {
+          abstractMethods.add(entry);
+        }
+      }
+    }
+
     private static SingleResolutionResult<?> singleResultHelper(
         DexClass initialResolutionResult, Entry<DexClass, DexEncodedMethod> entry) {
       return MethodResolutionResult.createSingleResolutionResult(
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/itf/ClassProcessor.java b/src/main/java/com/android/tools/r8/ir/desugar/itf/ClassProcessor.java
index 92467a3..16821510 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/itf/ClassProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/itf/ClassProcessor.java
@@ -49,6 +49,7 @@
 import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.BiConsumer;
@@ -656,6 +657,27 @@
           && !emulatedInterfaceInfo.contains(resolvedHolder.type)) {
         return true;
       }
+      // If the method overrides an abstract method which does not match the criteria, then
+      // forwarding methods are required so the invokes on the top level holder can resolve at
+      // runtime. See b/202188674.
+      if (overridesAbstractNonLibraryInterfaceMethod(
+          clazz, signature.get(), emulatedInterfaceInfo)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private boolean overridesAbstractNonLibraryInterfaceMethod(
+      DexClass clazz, DexMethod dexMethod, EmulatedInterfaceInfo emulatedInterfaceInfo) {
+    List<Entry<DexClass, DexEncodedMethod>> abstractInterfaceMethods =
+        appView.appInfoForDesugaring().getAbstractInterfaceMethods(clazz, dexMethod);
+    for (Entry<DexClass, DexEncodedMethod> classAndMethod : abstractInterfaceMethods) {
+      DexClass foundMethodClass = classAndMethod.getKey();
+      if (!foundMethodClass.isLibraryClass()
+          && !emulatedInterfaceInfo.contains(foundMethodClass.type)) {
+        return true;
+      }
     }
     return false;
   }
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/TelemetryMapInterfaceTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/TelemetryMapInterfaceTest.java
new file mode 100644
index 0000000..174c311
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/TelemetryMapInterfaceTest.java
@@ -0,0 +1,81 @@
+// Copyright (c) 2022, 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;
+
+import static com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification.SPECIFICATIONS_WITH_CF2CF;
+import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.getJdk8Jdk11;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification;
+import com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification;
+import com.android.tools.r8.utils.StringUtils;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiConsumer;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+// This is a reproduction for b/202188674.
+@RunWith(Parameterized.class)
+public class TelemetryMapInterfaceTest extends DesugaredLibraryTestBase {
+
+  private static final String EXPECTED_OUTPUT =
+      StringUtils.lines("k=key1;v=value1", "k=key1;v=value1", "k=key1;v=value1", "k=key1;v=value1");
+
+  private final TestParameters parameters;
+  private final CompilationSpecification compilationSpecification;
+  private final LibraryDesugaringSpecification libraryDesugaringSpecification;
+
+  @Parameters(name = "{0}, spec: {1}, {2}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build(),
+        getJdk8Jdk11(),
+        SPECIFICATIONS_WITH_CF2CF);
+  }
+
+  public TelemetryMapInterfaceTest(
+      TestParameters parameters,
+      LibraryDesugaringSpecification libraryDesugaringSpecification,
+      CompilationSpecification compilationSpecification) {
+    this.parameters = parameters;
+    this.compilationSpecification = compilationSpecification;
+    this.libraryDesugaringSpecification = libraryDesugaringSpecification;
+  }
+
+  @Test
+  public void testMap() throws Throwable {
+    testForDesugaredLibrary(parameters, libraryDesugaringSpecification, compilationSpecification)
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      Concrete concrete = new Concrete();
+      concrete.put("key1", "value1");
+      concrete.forEach((k, v) -> System.out.println("k=" + k + ";v=" + v));
+      MapInterface mapInterface = concrete;
+      mapInterface.forEach((k, v) -> System.out.println("k=" + k + ";v=" + v));
+      HashMap<String, Object> hashMap = concrete;
+      hashMap.forEach((k, v) -> System.out.println("k=" + k + ";v=" + v));
+      Map<String, Object> map = concrete;
+      map.forEach((k, v) -> System.out.println("k=" + k + ";v=" + v));
+    }
+  }
+
+  interface MapInterface {
+
+    void forEach(BiConsumer<? super String, ? super Object> consumer);
+  }
+
+  static class Concrete extends HashMap<String, Object> implements MapInterface {}
+}