Unbox enums through Objects#equals

Bug: b/185222995
Change-Id: Iafe49aacef449a1c7f6c7252ba73119c9a442131
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java
index bea580f..4838202 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java
@@ -1560,6 +1560,9 @@
         addRequiredNameData(enumClass);
         return Reason.ELIGIBLE;
       }
+      if (singleTargetReference == factory.objectsMethods.equals) {
+        return comparableAsUnboxedValues(invoke);
+      }
       return new UnsupportedLibraryInvokeReason(singleTargetReference);
     }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCfMethods.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCfMethods.java
index be89c4e..53e7b32 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCfMethods.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCfMethods.java
@@ -139,6 +139,38 @@
         ImmutableList.of());
   }
 
+  public static CfCode EnumUnboxingMethods_objectEquals(DexItemFactory factory, DexMethod method) {
+    CfLabel label0 = new CfLabel();
+    CfLabel label1 = new CfLabel();
+    CfLabel label2 = new CfLabel();
+    CfLabel label3 = new CfLabel();
+    return new CfCode(
+        method.holder,
+        2,
+        2,
+        ImmutableList.of(
+            label0,
+            new CfLoad(ValueType.INT, 0),
+            new CfLoad(ValueType.INT, 1),
+            new CfIfCmp(IfType.NE, ValueType.INT, label1),
+            new CfConstNumber(1, ValueType.INT),
+            new CfGoto(label2),
+            label1,
+            new CfFrame(
+                new Int2ObjectAVLTreeMap<>(
+                    new int[] {0, 1}, new FrameType[] {FrameType.intType(), FrameType.intType()})),
+            new CfConstNumber(0, ValueType.INT),
+            label2,
+            new CfFrame(
+                new Int2ObjectAVLTreeMap<>(
+                    new int[] {0, 1}, new FrameType[] {FrameType.intType(), FrameType.intType()}),
+                new ArrayDeque<>(Arrays.asList(FrameType.intType()))),
+            new CfReturn(ValueType.INT),
+            label3),
+        ImmutableList.of(),
+        ImmutableList.of());
+  }
+
   public static CfCode EnumUnboxingMethods_ordinal(DexItemFactory factory, DexMethod method) {
     CfLabel label0 = new CfLabel();
     CfLabel label1 = new CfLabel();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java
index a1f987b..05648c3 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java
@@ -501,6 +501,16 @@
         }
       } else if (invokedMethod == factory.objectsMethods.toStringWithObject) {
         rewriteStringValueOf(invoke, context, convertedEnums, instructionIterator, eventConsumer);
+      } else if (invokedMethod == factory.objectsMethods.equals) {
+        assert invoke.arguments().size() == 2;
+        Value argument = invoke.getFirstArgument();
+        DexType enumType = getEnumClassTypeOrNull(argument, convertedEnums);
+        if (enumType != null) {
+          replaceEnumInvoke(
+              instructionIterator,
+              invoke,
+              getSharedUtilityClass().ensureObjectsEqualsMethod(appView, context, eventConsumer));
+        }
       }
       return;
     }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/SharedEnumUnboxingUtilityClass.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/SharedEnumUnboxingUtilityClass.java
index 3792bd4..37c814d 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/SharedEnumUnboxingUtilityClass.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/SharedEnumUnboxingUtilityClass.java
@@ -72,6 +72,7 @@
     ensureCheckNotZeroWithMessageMethod(appView);
     ensureCompareToMethod(appView);
     ensureEqualsMethod(appView);
+    ensureObjectsEqualsMethod(appView);
     ensureOrdinalMethod(appView);
   }
 
@@ -151,6 +152,25 @@
         method -> EnumUnboxingCfMethods.EnumUnboxingMethods_equals(dexItemFactory, method));
   }
 
+  public ProgramMethod ensureObjectsEqualsMethod(
+      AppView<AppInfoWithLiveness> appView,
+      ProgramMethod context,
+      EnumUnboxerMethodProcessorEventConsumer eventConsumer) {
+    ProgramMethod method = ensureObjectsEqualsMethod(appView);
+    eventConsumer.acceptEnumUnboxerSharedUtilityClassMethodContext(method, context);
+    return method;
+  }
+
+  private ProgramMethod ensureObjectsEqualsMethod(AppView<AppInfoWithLiveness> appView) {
+    DexItemFactory dexItemFactory = appView.dexItemFactory();
+    return internalEnsureMethod(
+        appView,
+        dexItemFactory.createString("objects$equals"),
+        dexItemFactory.createProto(
+            dexItemFactory.booleanType, dexItemFactory.intType, dexItemFactory.intType),
+        method -> EnumUnboxingCfMethods.EnumUnboxingMethods_objectEquals(dexItemFactory, method));
+  }
+
   public ProgramMethod ensureOrdinalMethod(
       AppView<AppInfoWithLiveness> appView,
       ProgramMethod context,
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingMethods.java b/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingMethods.java
index cc2aa0c..48818ec 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingMethods.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingMethods.java
@@ -52,6 +52,12 @@
     return unboxedEnum1 == unboxedEnum2;
   }
 
+  // Objects#equals is similar to equals without the NPE for null entries.
+  // Objects.equals(null,null) answers true.
+  public static boolean objectEquals(int unboxedEnum1, int unboxedEnum2) {
+    return unboxedEnum1 == unboxedEnum2;
+  }
+
   // Methods zeroCheck and zeroCheckMessage are used to replace null checks on unboxed enums.
   public static void zeroCheck(int unboxedEnum) {
     if (unboxedEnum == 0) {
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/EqualsCompareToEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/EqualsCompareToEnumUnboxingTest.java
index 82689d3..172e118 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/EqualsCompareToEnumUnboxingTest.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/EqualsCompareToEnumUnboxingTest.java
@@ -5,8 +5,10 @@
 package com.android.tools.r8.enumunboxing;
 
 import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.TestParameters;
 import java.util.List;
+import java.util.Objects;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -40,6 +42,7 @@
         .addEnumUnboxingInspector(
             inspector -> inspector.assertUnboxed(EnumEqualscompareTo.MyEnum.class))
         .enableNeverClassInliningAnnotations()
+        .enableInliningAnnotations()
         .addKeepRules(enumKeepRules.getKeepRules())
         .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
         .setMinApi(parameters)
@@ -59,9 +62,31 @@
 
     public static void main(String[] args) {
       equalsTest();
+      objectsEqualsTest();
       compareToTest();
     }
 
+    @NeverInline
+    @SuppressWarnings({"ConstantConditions", "EqualsWithItself", "ResultOfMethodCallIgnored"})
+    private static void objectsEqualsTest() {
+      System.out.println(performObjectsEquals(MyEnum.A, MyEnum.B));
+      System.out.println(false);
+      System.out.println(performObjectsEquals(MyEnum.A, MyEnum.A));
+      System.out.println(true);
+      System.out.println(performObjectsEquals(MyEnum.A, null));
+      System.out.println(false);
+      System.out.println(performObjectsEquals(null, MyEnum.A));
+      System.out.println(false);
+      System.out.println(performObjectsEquals(null, null));
+      System.out.println(true);
+    }
+
+    @NeverInline
+    private static boolean performObjectsEquals(MyEnum a, MyEnum b) {
+      return Objects.equals(a, b);
+    }
+
+    @NeverInline
     @SuppressWarnings({"ConstantConditions", "EqualsWithItself", "ResultOfMethodCallIgnored"})
     private static void equalsTest() {
       System.out.println(MyEnum.A.equals(MyEnum.B));
@@ -78,6 +103,7 @@
       }
     }
 
+    @NeverInline
     @SuppressWarnings({"ConstantConditions", "EqualsWithItself", "ResultOfMethodCallIgnored"})
     private static void compareToTest() {
       System.out.println(MyEnum.B.compareTo(MyEnum.A) > 0);