Mitigate issues from invoke-virtual to classes that were once interfaces

Bug: 206891715
Change-Id: I7532d3f51acacd16a3cde62f65e8e881cd9aebd8
diff --git a/src/main/java/com/android/tools/r8/graph/analysis/InvokeVirtualToInterfaceVerifyErrorWorkaround.java b/src/main/java/com/android/tools/r8/graph/analysis/InvokeVirtualToInterfaceVerifyErrorWorkaround.java
new file mode 100644
index 0000000..7c17e4c
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/analysis/InvokeVirtualToInterfaceVerifyErrorWorkaround.java
@@ -0,0 +1,89 @@
+// 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.graph.analysis;
+
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.shaking.Enqueuer;
+import com.android.tools.r8.shaking.KeepInfo.Joiner;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.InternalOptions;
+
+/**
+ * Workaround for situations where an interface in the framework has been changed into a class. When
+ * compiling against a recent android.jar, virtual invokes to such classes may resolve to interface
+ * methods on older API levels, which can lead to verification errors (see b/206891715).
+ *
+ * <p>To mitigate this issue, we pin methods that have virtual invokes to such classes. This ensures
+ * that we don't move these virtual invokes elsewhere (e.g., due to inlining or class merging),
+ * which would otherwise lead to verification errors in other classes.
+ */
+// TODO(b/206891715): This only mitigates the issue. The user may still need to manually outline
+//  virtual invokes to classes that was once an interface. To avoid this in general (including D8)
+//  the compiler should outline the problematic invokes.
+public class InvokeVirtualToInterfaceVerifyErrorWorkaround implements EnqueuerInvokeAnalysis {
+
+  private final DexType androidHardwareCamera2CameraDeviceType;
+  private final Enqueuer enqueuer;
+  private final InternalOptions options;
+
+  public InvokeVirtualToInterfaceVerifyErrorWorkaround(
+      AppView<? extends AppInfoWithClassHierarchy> appView, Enqueuer enqueuer) {
+    this.androidHardwareCamera2CameraDeviceType =
+        appView.dexItemFactory().createType("Landroid/hardware/camera2/CameraDevice;");
+    this.enqueuer = enqueuer;
+    this.options = appView.options();
+  }
+
+  public static void register(
+      AppView<? extends AppInfoWithClassHierarchy> appView, Enqueuer enqueuer) {
+    if (!isNoop(appView)) {
+      enqueuer.registerInvokeAnalysis(
+          new InvokeVirtualToInterfaceVerifyErrorWorkaround(appView, enqueuer));
+    }
+  }
+
+  private static boolean isNoop(AppView<? extends AppInfoWithClassHierarchy> appView) {
+    return appView.options().getMinApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.L);
+  }
+
+  @Override
+  public void traceInvokeVirtual(DexMethod invokedMethod, ProgramMethod context) {
+    if (isInterfaceInSomeApiLevel(invokedMethod.getHolderType())) {
+      enqueuer.getKeepInfo().joinMethod(context, Joiner::disallowOptimization);
+    }
+  }
+
+  private boolean isInterfaceInSomeApiLevel(DexType type) {
+    // CameraDevice was added as a class in API 21 (L), but was defined as an interface in the
+    // framework before then.
+    return type == androidHardwareCamera2CameraDeviceType
+        && (options.isGeneratingClassFiles()
+            || options.getMinApiLevel().isLessThan(AndroidApiLevel.L));
+  }
+
+  @Override
+  public void traceInvokeDirect(DexMethod invokedMethod, ProgramMethod context) {
+    // Intentionally empty.
+  }
+
+  @Override
+  public void traceInvokeInterface(DexMethod invokedMethod, ProgramMethod context) {
+    // Intentionally empty.
+  }
+
+  @Override
+  public void traceInvokeStatic(DexMethod invokedMethod, ProgramMethod context) {
+    // Intentionally empty.
+  }
+
+  @Override
+  public void traceInvokeSuper(DexMethod invokedMethod, ProgramMethod context) {
+    // Intentionally empty.
+  }
+}
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 33af67f..02c18fc 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -81,6 +81,7 @@
 import com.android.tools.r8.graph.analysis.EnqueuerExceptionGuardAnalysis;
 import com.android.tools.r8.graph.analysis.EnqueuerInstanceOfAnalysis;
 import com.android.tools.r8.graph.analysis.EnqueuerInvokeAnalysis;
+import com.android.tools.r8.graph.analysis.InvokeVirtualToInterfaceVerifyErrorWorkaround;
 import com.android.tools.r8.ir.analysis.proto.ProtoEnqueuerUseRegistry;
 import com.android.tools.r8.ir.analysis.proto.schema.ProtoEnqueuerExtension;
 import com.android.tools.r8.ir.code.ArrayPut;
@@ -231,11 +232,11 @@
   private final boolean forceProguardCompatibility;
   private final Mode mode;
 
-  private Set<EnqueuerAnalysis> analyses = Sets.newIdentityHashSet();
-  private Set<EnqueuerInvokeAnalysis> invokeAnalyses = Sets.newIdentityHashSet();
-  private Set<EnqueuerInstanceOfAnalysis> instanceOfAnalyses = Sets.newIdentityHashSet();
-  private Set<EnqueuerExceptionGuardAnalysis> exceptionGuardAnalyses = Sets.newIdentityHashSet();
-  private Set<EnqueuerCheckCastAnalysis> checkCastAnalyses = Sets.newIdentityHashSet();
+  private Set<EnqueuerAnalysis> analyses = new LinkedHashSet<>();
+  private Set<EnqueuerInvokeAnalysis> invokeAnalyses = new LinkedHashSet<>();
+  private Set<EnqueuerInstanceOfAnalysis> instanceOfAnalyses = new LinkedHashSet<>();
+  private Set<EnqueuerExceptionGuardAnalysis> exceptionGuardAnalyses = new LinkedHashSet<>();
+  private Set<EnqueuerCheckCastAnalysis> checkCastAnalyses = new LinkedHashSet<>();
 
   // Don't hold a direct pointer to app info (use appView).
   private AppInfoWithClassHierarchy appInfo;
@@ -470,6 +471,7 @@
             : null;
 
     if (mode.isInitialOrFinalTreeShaking()) {
+      InvokeVirtualToInterfaceVerifyErrorWorkaround.register(appView, this);
       if (options.protoShrinking().enableGeneratedMessageLiteShrinking) {
         registerAnalysis(new ProtoEnqueuerExtension(appView));
       }
@@ -528,7 +530,7 @@
     return this;
   }
 
-  private Enqueuer registerInvokeAnalysis(EnqueuerInvokeAnalysis analysis) {
+  public Enqueuer registerInvokeAnalysis(EnqueuerInvokeAnalysis analysis) {
     invokeAnalyses.add(analysis);
     return this;
   }
@@ -671,6 +673,10 @@
     return clazz;
   }
 
+  public MutableKeepInfoCollection getKeepInfo() {
+    return keepInfo;
+  }
+
   public KeepClassInfo getKeepInfo(DexProgramClass clazz) {
     return keepInfo.getClassInfo(clazz);
   }
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 2da3135..672fd24 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -569,6 +569,10 @@
   private final boolean enableTreeShaking;
   private final boolean enableMinification;
 
+  public AndroidApiLevel getMinApiLevel() {
+    return minApiLevel;
+  }
+
   public boolean isOptimizing() {
     return hasProguardConfiguration() && getProguardConfiguration().isOptimizing();
   }
diff --git a/src/test/java/com/android/tools/r8/workaround/InvokeVirtualToInterfaceVerifyErrorWorkaroundTest.java b/src/test/java/com/android/tools/r8/workaround/InvokeVirtualToInterfaceVerifyErrorWorkaroundTest.java
new file mode 100644
index 0000000..3cc8749
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/workaround/InvokeVirtualToInterfaceVerifyErrorWorkaroundTest.java
@@ -0,0 +1,125 @@
+// 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.workaround;
+
+import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethodWithName;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class InvokeVirtualToInterfaceVerifyErrorWorkaroundTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection parameters() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(A.class)
+        .addProgramClassFileData(getProgramClassFileData())
+        .addKeepClassAndMembersRules(Main.class)
+        // CameraDevice is not present in rt.jar or android.jar when API<L.
+        .applyIf(
+            parameters.isCfRuntime() || parameters.getApiLevel().isLessThan(AndroidApiLevel.L),
+            testBuilder -> testBuilder.addDontWarn("android.hardware.camera2.CameraDevice"))
+        // CameraDeviceUser can only be merged with A when min API>=L.
+        .addHorizontallyMergedClassesInspector(
+            inspector ->
+                inspector
+                    .applyIf(
+                        parameters.isDexRuntime()
+                            && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.L),
+                        i -> i.assertIsCompleteMergeGroup(A.class, CameraDeviceUser.class))
+                    .assertNoOtherClassesMerged())
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        // CameraDeviceUser.m() can only be inlined when min API>=L.
+        .inspect(
+            inspector -> {
+              if (parameters.isDexRuntime()
+                  && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.L)) {
+                assertThat(inspector.clazz(CameraDeviceUser.class), isAbsent());
+              } else {
+                ClassSubject cameraDeviceUserClassSubject = inspector.clazz(CameraDeviceUser.class);
+                assertThat(cameraDeviceUserClassSubject, isPresent());
+
+                MethodSubject mMethodSubject =
+                    cameraDeviceUserClassSubject.uniqueMethodWithName("m");
+                assertThat(mMethodSubject, isPresent());
+                assertThat(mMethodSubject, invokesMethodWithName("close"));
+              }
+            });
+  }
+
+  private static List<byte[]> getProgramClassFileData() throws IOException {
+    String oldDescriptor = DescriptorUtils.javaTypeToDescriptor(CameraDevice.class.getTypeName());
+    String newDescriptor = "Landroid/hardware/camera2/CameraDevice;";
+    return ImmutableList.of(
+        transformer(Main.class)
+            .replaceClassDescriptorInMembers(oldDescriptor, newDescriptor)
+            .replaceClassDescriptorInMethodInstructions(oldDescriptor, newDescriptor)
+            .transform(),
+        transformer(CameraDeviceUser.class)
+            .replaceClassDescriptorInMembers(oldDescriptor, newDescriptor)
+            .replaceClassDescriptorInMethodInstructions(oldDescriptor, newDescriptor)
+            .transform());
+  }
+
+  // The following classes are transformed to use android.hardware.camera2.CameraDevice.
+  static class Main {
+
+    public static void main(String[] args) {
+      int minApi = args.length;
+      if (minApi >= 21) {
+        new CameraDeviceUser().m(getCameraDevice());
+      } else {
+        System.out.println(new A());
+      }
+    }
+
+    // @Keep
+    static CameraDevice getCameraDevice() {
+      return null;
+    }
+  }
+
+  static class A {}
+
+  static class CameraDeviceUser {
+
+    public void m(CameraDevice cd) {
+      if (cd != null) {
+        cd.close();
+      }
+    }
+  }
+
+  abstract static class CameraDevice {
+
+    abstract void close();
+  }
+}