Extend and disable-by-default Enum reflection tracing

Enum.valueOf and Enum collections will no longer cause Enum.values()
methods to be kept by default.

The logic is now guarded by a system property:
com.android.tools.r8.experimentalTraceEnumReflection

It now also recognizes:
 * Bundle.getSerializable()
 * Parcel.readSerializable()
 * Intent.getSerializableExtra()
 * ObjectInputStream.readObject()

Some limitations:
 * For the overloads that accept a Class, it must be a const-class
 * For other overloads, the return value must be directly used by a
   checked-cast of the Enum type.

Bug: b/204939965
Change-Id: Icd7b273ebdea62c19917e9e5ecac6089849a536e
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index 27f31fa..1aa0120 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -640,9 +640,6 @@
       createStaticallyKnownType("Ljava/util/logging/Level;");
   public final DexType javaUtilLoggingLoggerType =
       createStaticallyKnownType("Ljava/util/logging/Logger;");
-  public final DexType javaUtilEnumMapType = createStaticallyKnownType("Ljava/util/EnumMap;");
-  public final DexType javaUtilEnumSetType = createStaticallyKnownType("Ljava/util/EnumSet;");
-
   public final DexType androidAppActivity = createStaticallyKnownType("Landroid/app/Activity;");
   public final DexType androidAppFragment = createStaticallyKnownType("Landroid/app/Fragment;");
   public final DexType androidAppZygotePreload =
@@ -764,8 +761,6 @@
   public final JavaUtilLocaleMembers javaUtilLocaleMembers = new JavaUtilLocaleMembers();
   public final JavaUtilLoggingLevelMembers javaUtilLoggingLevelMembers =
       new JavaUtilLoggingLevelMembers();
-  public final JavaUtilEnumMapMembers javaUtilEnumMapMembers = new JavaUtilEnumMapMembers();
-  public final JavaUtilEnumSetMembers javaUtilEnumSetMembers = new JavaUtilEnumSetMembers();
 
   public final List<LibraryMembers> libraryMembersCollection =
       ImmutableList.of(
@@ -1614,28 +1609,6 @@
     }
   }
 
-  public class JavaUtilEnumMapMembers {
-    public final DexMethod constructor =
-        createMethod(javaUtilEnumMapType, createProto(voidType, classType), constructorMethodName);
-  }
-
-  public class JavaUtilEnumSetMembers {
-    private final DexString allOfString = createString("allOf");
-    private final DexString noneOfString = createString("noneOf");
-    private final DexString rangeString = createString("range");
-
-    public boolean isFactoryMethod(DexMethod invokedMethod) {
-      if (!invokedMethod.getHolderType().equals(javaUtilEnumSetType)) {
-        return false;
-      }
-      DexString name = invokedMethod.getName();
-      return name.isIdenticalTo(allOfString)
-          || name.isIdenticalTo(noneOfString)
-          || name.isIdenticalTo(ofMethodName)
-          || name.isIdenticalTo(rangeString);
-    }
-  }
-
   public class LongMembers extends BoxedPrimitiveMembers {
 
     public final DexField TYPE = createField(boxedLongType, classType, "TYPE");
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 fdc7fb5..d179979 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -99,9 +99,7 @@
 import com.android.tools.r8.ir.analysis.proto.GeneratedMessageLiteBuilderShrinker;
 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.analysis.type.ClassTypeElement;
 import com.android.tools.r8.ir.code.ArrayPut;
-import com.android.tools.r8.ir.code.ConstClass;
 import com.android.tools.r8.ir.code.ConstantValueUtils;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
@@ -553,6 +551,10 @@
       ProtoEnqueuerExtension.register(appView, analysesBuilder);
       ResourceAccessAnalysis.register(appView, this, analysesBuilder);
       RuntimeTypeCheckInfo.register(runtimeTypeCheckInfoBuilder, analysesBuilder);
+      if (options.experimentalTraceEnumReflection) {
+        EnqueuerEnumReflectionSupport.register(
+            appView, this::markEnumValuesAsReachable, analysesBuilder);
+      }
     }
     analyses = analysesBuilder.build();
 
@@ -1548,11 +1550,6 @@
     MethodResolutionResult resolutionResult =
         handleInvokeOfDirectTarget(invokedMethod, context, reason);
     analyses.traceInvokeDirect(invokedMethod, resolutionResult, context);
-
-    if (invokedMethod.equals(appView.dexItemFactory().javaUtilEnumMapMembers.constructor)) {
-      // EnumMap uses reflection.
-      pendingReflectiveUses.add(context);
-    }
   }
 
   void traceInvokeInterface(
@@ -1607,10 +1604,6 @@
       identifierNameStrings.add(invokedMethod);
       // Revisit the current method to implicitly add -keep rule for items with reflective access.
       pendingReflectiveUses.add(context);
-    } else if (invokedMethod == dexItemFactory.enumMembers.valueOf
-        || dexItemFactory.javaUtilEnumSetMembers.isFactoryMethod(invokedMethod)) {
-      // See comment in handleEnumValueOfOrCollectionInstantiation.
-      pendingReflectiveUses.add(context);
     } else if (invokedMethod == dexItemFactory.proxyMethods.newProxyInstance) {
       pendingReflectiveUses.add(context);
     } else if (dexItemFactory.serviceLoaderMethods.isLoadMethod(invokedMethod)) {
@@ -1647,7 +1640,6 @@
         invokedMethod, context, registry, KeepReason.invokedFromLambdaCreatedIn(context));
   }
 
-  @SuppressWarnings("ReferenceEquality")
   private void traceInvokeVirtual(
       DexMethod invokedMethod,
       ProgramMethod context,
@@ -1656,10 +1648,11 @@
     if (registry != null && !registry.markInvokeVirtualAsSeen(invokedMethod)) {
       return;
     }
-    if (invokedMethod == appView.dexItemFactory().classMethods.newInstance
-        || invokedMethod == appView.dexItemFactory().constructorMethods.newInstance) {
+    DexItemFactory dexItemFactory = appView.dexItemFactory();
+    if (invokedMethod.isIdenticalTo(dexItemFactory.classMethods.newInstance)
+        || invokedMethod.isIdenticalTo(dexItemFactory.constructorMethods.newInstance)) {
       pendingReflectiveUses.add(context);
-    } else if (appView.dexItemFactory().classMethods.isReflectiveMemberLookup(invokedMethod)) {
+    } else if (dexItemFactory.classMethods.isReflectiveMemberLookup(invokedMethod)) {
       // Implicitly add -identifiernamestring rule for the Java reflection in use.
       identifierNameStrings.add(invokedMethod);
       // Revisit the current method to implicitly add -keep rule for items with reflective access.
@@ -5224,33 +5217,24 @@
     InstructionIterator iterator = code.instructionIterator();
     while (iterator.hasNext()) {
       Instruction instruction = iterator.next();
-      handleReflectiveBehavior(method, instruction);
+      if (instruction.isInvokeMethod()) {
+        handleReflectiveBehavior(method, instruction.asInvokeMethod());
+      }
     }
   }
 
-  @SuppressWarnings("ReferenceEquality")
-  private void handleReflectiveBehavior(ProgramMethod method, Instruction instruction) {
-    if (!instruction.isInvokeMethod()) {
-      return;
-    }
-    InvokeMethod invoke = instruction.asInvokeMethod();
+  private void handleReflectiveBehavior(ProgramMethod method, InvokeMethod invoke) {
     DexMethod invokedMethod = invoke.getInvokedMethod();
     DexItemFactory dexItemFactory = appView.dexItemFactory();
-    if (invokedMethod == dexItemFactory.classMethods.newInstance) {
+    if (invokedMethod.isIdenticalTo(dexItemFactory.classMethods.newInstance)) {
       handleJavaLangClassNewInstance(method, invoke);
       return;
     }
-    if (invokedMethod == dexItemFactory.constructorMethods.newInstance) {
+    if (invokedMethod.isIdenticalTo(dexItemFactory.constructorMethods.newInstance)) {
       handleJavaLangReflectConstructorNewInstance(method, invoke);
       return;
     }
-    if (invokedMethod == dexItemFactory.enumMembers.valueOf
-        || invokedMethod == dexItemFactory.javaUtilEnumMapMembers.constructor
-        || dexItemFactory.javaUtilEnumSetMembers.isFactoryMethod(invokedMethod)) {
-      handleEnumValueOfOrCollectionInstantiation(method, invoke);
-      return;
-    }
-    if (invokedMethod == dexItemFactory.proxyMethods.newProxyInstance) {
+    if (invokedMethod.isIdenticalTo(dexItemFactory.proxyMethods.newProxyInstance)) {
       handleJavaLangReflectProxyNewProxyInstance(method, invoke);
       return;
     }
@@ -5573,47 +5557,6 @@
     }
   }
 
-  private void handleEnumValueOfOrCollectionInstantiation(
-      ProgramMethod context, InvokeMethod invoke) {
-    if (invoke.inValues().isEmpty()) {
-      // Should never happen.
-      return;
-    }
-
-    // The use of java.lang.Enum.valueOf(java.lang.Class, java.lang.String) will indirectly
-    // access the values() method of the enum class passed as the first argument. The method
-    // SomeEnumClass.valueOf(java.lang.String) which is generated by javac for all enums will
-    // call this method.
-    // Likewise, EnumSet and EnumMap call values() on the passed in Class.
-    Value firstArg = invoke.getFirstNonReceiverArgument();
-    if (firstArg.isPhi()) {
-      return;
-    }
-    DexType type;
-    if (invoke
-        .getInvokedMethod()
-        .getParameter(0)
-        .isIdenticalTo(appView.dexItemFactory().classType)) {
-      // EnumMap.<init>(), EnumSet.noneOf(), EnumSet.allOf(), Enum.valueOf().
-      ConstClass constClass = firstArg.definition.asConstClass();
-      if (constClass == null || !constClass.getType().isClassType()) {
-        return;
-      }
-      type = constClass.getType();
-    } else {
-      // EnumSet.of(), EnumSet.range()
-      ClassTypeElement typeElement = firstArg.getType().asClassType();
-      if (typeElement == null) {
-        return;
-      }
-      type = typeElement.getClassType();
-    }
-    DexProgramClass clazz = getProgramClassOrNull(type, context);
-    if (clazz != null && clazz.isEnum()) {
-      markEnumValuesAsReachable(clazz, KeepReason.invokedFrom(context));
-    }
-  }
-
   private void handleServiceLoaderInvocation(ProgramMethod method, InvokeMethod invoke) {
     if (invoke.inValues().isEmpty()) {
       // Should never happen.
diff --git a/src/main/java/com/android/tools/r8/shaking/EnqueuerEnumReflectionSupport.java b/src/main/java/com/android/tools/r8/shaking/EnqueuerEnumReflectionSupport.java
new file mode 100644
index 0000000..376a723
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/EnqueuerEnumReflectionSupport.java
@@ -0,0 +1,336 @@
+// Copyright (c) 2025, 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.shaking;
+
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexItemFactory;
+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;
+import com.android.tools.r8.graph.MethodResolutionResult;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.analysis.EnqueuerAnalysisCollection;
+import com.android.tools.r8.graph.analysis.FixpointEnqueuerAnalysis;
+import com.android.tools.r8.graph.analysis.TraceInvokeEnqueuerAnalysis;
+import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
+import com.android.tools.r8.ir.code.CheckCast;
+import com.android.tools.r8.ir.code.ConstClass;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InstructionIterator;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.MethodConversionOptions;
+import com.android.tools.r8.utils.Timing;
+import com.android.tools.r8.utils.collections.ProgramMethodSet;
+import java.util.concurrent.ExecutorService;
+import java.util.function.BiConsumer;
+
+/**
+ * Finds Enum.valueOf() methods that must be kept due to uses of OS APIs that use it via reflection.
+ *
+ * <p>Looks for:
+ *
+ * <ul>
+ *   <li>Enum.valueOf()
+ *   <li>EnumSet.allOf()
+ *   <li>EnumSet.noneOf()
+ *   <li>EnumSet.range()
+ *   <li>new EnumMap()
+ *   <li>Parcel.readSerializable()
+ *   <li>Bundle.getSerializable()
+ *   <li>Intent.getSerializableExtra()
+ *   <li>ObjectInputStream.readObject()
+ * </ul>
+ *
+ * <p>Similar to non-enum reflection calls, tracing is best-effort. E.g.:
+ *
+ * <ul>
+ *   <li>For the overloads that accept a Class, it must be a const-class
+ *   <li>For others, the return value must be directly used by a checked-cast of the Enum type.
+ * </ul>
+ */
+public class EnqueuerEnumReflectionSupport
+    implements TraceInvokeEnqueuerAnalysis, FixpointEnqueuerAnalysis {
+  private final AppView<? extends AppInfoWithClassHierarchy> appView;
+  private final BiConsumer<DexProgramClass, KeepReason> markAsReachableCallback;
+
+  private final DexString enumSetAllOfString;
+  private final DexString enumSetNoneOfString;
+  private final DexString enumSetRangeString;
+  private final DexString enumSetOfString;
+  private final DexType androidContentIntentType;
+  private final DexType androidOsParcelType;
+  private final DexType javaIoObjectInputStreamType;
+  private final DexType javaUtilEnumMapType;
+  private final DexType javaUtilEnumSetType;
+
+  private final DexMethod intentGetSerializableExtra1;
+  private final DexMethod intentGetSerializableExtra2;
+  private final DexMethod bundleGetSerializable1;
+  private final DexMethod bundleGetSerializable2;
+  private final DexMethod parcelReadSerializable1;
+  private final DexMethod parcelReadSerializable2;
+
+  private final DexMethod objectInputStreamReadObject;
+  private final DexMethod enumValueOfMethod;
+  private final DexMethod enumMapConstructor;
+  private final ProgramMethodSet pendingReflectiveUses = ProgramMethodSet.createLinked();
+
+  public EnqueuerEnumReflectionSupport(
+      AppView<? extends AppInfoWithClassHierarchy> appView,
+      BiConsumer<DexProgramClass, KeepReason> markAsReachableCallback) {
+    this.appView = appView;
+    this.markAsReachableCallback = markAsReachableCallback;
+
+    DexItemFactory dexItemFactory = appView.dexItemFactory();
+
+    enumSetAllOfString = dexItemFactory.createString("allOf");
+    enumSetNoneOfString = dexItemFactory.createString("noneOf");
+    enumSetRangeString = dexItemFactory.createString("range");
+    enumSetOfString = dexItemFactory.ofMethodName;
+
+    androidContentIntentType = dexItemFactory.createType("Landroid/content/Intent;");
+    androidOsParcelType = dexItemFactory.createType("Landroid/os/Parcel;");
+    javaIoObjectInputStreamType = dexItemFactory.createType("Ljava/io/ObjectInputStream;");
+    javaUtilEnumMapType = dexItemFactory.createType("Ljava/util/EnumMap;");
+    javaUtilEnumSetType = dexItemFactory.createType("Ljava/util/EnumSet;");
+
+    intentGetSerializableExtra1 =
+        dexItemFactory.createMethod(
+            androidContentIntentType,
+            dexItemFactory.createProto(dexItemFactory.serializableType, dexItemFactory.stringType),
+            "getSerializableExtra");
+    intentGetSerializableExtra2 =
+        dexItemFactory.createMethod(
+            androidContentIntentType,
+            dexItemFactory.createProto(
+                dexItemFactory.serializableType,
+                dexItemFactory.stringType,
+                dexItemFactory.classType),
+            "getSerializableExtra");
+    bundleGetSerializable1 =
+        dexItemFactory.createMethod(
+            dexItemFactory.androidOsBundleType,
+            dexItemFactory.createProto(dexItemFactory.serializableType, dexItemFactory.stringType),
+            "getSerializable");
+    bundleGetSerializable2 =
+        dexItemFactory.createMethod(
+            dexItemFactory.androidOsBundleType,
+            dexItemFactory.createProto(
+                dexItemFactory.serializableType,
+                dexItemFactory.stringType,
+                dexItemFactory.classType),
+            "getSerializable");
+    parcelReadSerializable1 =
+        dexItemFactory.createMethod(
+            androidOsParcelType,
+            dexItemFactory.createProto(dexItemFactory.serializableType),
+            "readSerializable");
+    parcelReadSerializable2 =
+        dexItemFactory.createMethod(
+            androidOsParcelType,
+            dexItemFactory.createProto(
+                dexItemFactory.serializableType,
+                dexItemFactory.classLoaderType,
+                dexItemFactory.classType),
+            "readSerializable");
+    objectInputStreamReadObject =
+        dexItemFactory.createMethod(
+            javaIoObjectInputStreamType,
+            dexItemFactory.createProto(dexItemFactory.objectType),
+            "readObject");
+    enumValueOfMethod = dexItemFactory.enumMembers.valueOf;
+    enumMapConstructor =
+        dexItemFactory.createMethod(
+            javaUtilEnumMapType,
+            dexItemFactory.createProto(dexItemFactory.voidType, dexItemFactory.classType),
+            dexItemFactory.constructorMethodName);
+  }
+
+  public static void register(
+      AppView<? extends AppInfoWithClassHierarchy> appView,
+      BiConsumer<DexProgramClass, KeepReason> markAsReachableCallback,
+      EnqueuerAnalysisCollection.Builder builder) {
+    EnqueuerEnumReflectionSupport instance =
+        new EnqueuerEnumReflectionSupport(appView, markAsReachableCallback);
+    builder.addTraceInvokeAnalysis(instance);
+    builder.addFixpointAnalysis(instance);
+  }
+
+  private DexProgramClass maybeGetProgramEnumType(DexType type, boolean checkSuper) {
+    // Arrays can be used for serialization-related methods.
+    if (type.isArrayType()) {
+      type = type.toBaseType(appView.dexItemFactory());
+      if (!type.isClassType()) {
+        return null;
+      }
+    }
+
+    DexClass dexClass = appView.definitionFor(type);
+    if (dexClass == null || !dexClass.isProgramClass() || !dexClass.hasSuperType()) {
+      return null;
+    }
+    DexType superType = dexClass.getSuperType();
+    if (superType.isIdenticalTo(appView.dexItemFactory().enumType)) {
+      return dexClass.asProgramClass();
+    }
+    // Cannot have a sub-sub-type of an Enum.
+    return checkSuper ? maybeGetProgramEnumType(superType, false) : null;
+  }
+
+  private void handleEnumCastFromSerializable(ProgramMethod context, Value value) {
+    if (value == null || value.isPhi()) {
+      return;
+    }
+    for (Instruction user : value.aliasedUsers()) {
+      CheckCast castInstr = user.asCheckCast();
+      if (castInstr == null) {
+        continue;
+      }
+      DexProgramClass enumClass = maybeGetProgramEnumType(castInstr.getType(), true);
+      if (enumClass != null) {
+        markAsReachableCallback.accept(enumClass, KeepReason.invokedFrom(context));
+      }
+    }
+  }
+
+  private void handleReflectiveEnumInvoke(
+      ProgramMethod context, InvokeMethod invoke, int classParamIndex) {
+    if (invoke.inValues().size() <= classParamIndex) {
+      // Should never happen.
+      return;
+    }
+
+    // The use of java.lang.Enum.valueOf(java.lang.Class, java.lang.String) will indirectly
+    // access the values() method of the enum class passed as the first argument. The method
+    // SomeEnumClass.valueOf(java.lang.String) which is generated by javac for all enums will
+    // call this method.
+    // Likewise, EnumSet and EnumMap call values() on the passed in Class.
+    Value classArg = invoke.getArgumentForParameter(classParamIndex);
+    if (classArg.isPhi()) {
+      return;
+    }
+    DexType type;
+    if (invoke
+        .getInvokedMethod()
+        .getParameter(classParamIndex)
+        .isIdenticalTo(appView.dexItemFactory().classType)) {
+      // EnumMap.<init>(), EnumSet.noneOf(), EnumSet.allOf(), Enum.valueOf().
+      ConstClass constClass = classArg.definition.asConstClass();
+      if (constClass == null) {
+        return;
+      }
+      type = constClass.getType();
+    } else {
+      // EnumSet.of(), EnumSet.range()
+      ClassTypeElement typeElement = classArg.getType().asClassType();
+      if (typeElement == null) {
+        return;
+      }
+      type = typeElement.getClassType();
+    }
+    // Arrays can be used for serialization-related methods.
+    if (type.isArrayType()) {
+      type = type.toBaseType(appView.dexItemFactory());
+    }
+    DexClass clazz = appView.definitionFor(type, context);
+    if (clazz != null && clazz.isProgramClass() && clazz.isEnum()) {
+      markAsReachableCallback.accept(clazz.asProgramClass(), KeepReason.invokedFrom(context));
+    }
+  }
+
+  private void handleReflectiveBehavior(ProgramMethod method) {
+    IRCode code = method.buildIR(appView, MethodConversionOptions.nonConverting());
+    InstructionIterator iterator = code.instructionIterator();
+    while (iterator.hasNext()) {
+      Instruction instruction = iterator.next();
+      if (!instruction.isInvokeMethod()) {
+        continue;
+      }
+      InvokeMethod invoke = instruction.asInvokeMethod();
+      DexMethod invokedMethod = invoke.getInvokedMethod();
+      if (invokedMethod.isIdenticalTo(enumValueOfMethod)
+          || invokedMethod.isIdenticalTo(enumMapConstructor)
+          || isEnumSetFactoryMethod(invokedMethod)) {
+        handleReflectiveEnumInvoke(method, invoke, 0);
+      } else if (invokedMethod.isIdenticalTo(bundleGetSerializable2)
+          || invokedMethod.isIdenticalTo(parcelReadSerializable2)
+          || invokedMethod.isIdenticalTo(intentGetSerializableExtra2)) {
+        handleReflectiveEnumInvoke(method, invoke, 1);
+      } else if (invokedMethod.isIdenticalTo(bundleGetSerializable1)
+          || invokedMethod.isIdenticalTo(parcelReadSerializable1)
+          || invokedMethod.isIdenticalTo(intentGetSerializableExtra1)
+          || invokedMethod.isIdenticalTo(objectInputStreamReadObject)) {
+        // These forms do not take a Class parameter, but are often followed by a checked-cast.
+        // Get the enum type from the checked-cast.
+        // There is no checked-cast when the return value is used as an Object (e.g. toString(), or
+        // inserted into a List<Object>).
+        // This also fails to identify when the above methods are wrapped in a helper method
+        // (e.g. a "IntentUtils.safeGetSerializableExtra()").
+        handleEnumCastFromSerializable(method, invoke.outValue());
+      }
+    }
+  }
+
+  private boolean isEnumSetFactoryMethod(DexMethod invokedMethod) {
+    if (!invokedMethod.getHolderType().equals(javaUtilEnumSetType)) {
+      return false;
+    }
+    DexString name = invokedMethod.getName();
+    return name.isIdenticalTo(enumSetAllOfString)
+        || name.isIdenticalTo(enumSetNoneOfString)
+        || name.isIdenticalTo(enumSetOfString)
+        || name.isIdenticalTo(enumSetRangeString);
+  }
+
+  @Override
+  public void traceInvokeStatic(
+      DexMethod invokedMethod, MethodResolutionResult resolutionResult, ProgramMethod context) {
+    if (invokedMethod.isIdenticalTo(enumValueOfMethod) || isEnumSetFactoryMethod(invokedMethod)) {
+      pendingReflectiveUses.add(context);
+    }
+  }
+
+  @Override
+  public void traceInvokeDirect(
+      DexMethod invokedMethod, MethodResolutionResult resolutionResult, ProgramMethod context) {
+    // EnumMap uses reflection.
+    if (invokedMethod.isIdenticalTo(enumMapConstructor)) {
+      pendingReflectiveUses.add(context);
+    }
+  }
+
+  @Override
+  public void traceInvokeVirtual(
+      DexMethod invokedMethod, MethodResolutionResult resolutionResult, ProgramMethod context) {
+    if (invokedMethod.isIdenticalTo(bundleGetSerializable1)
+        || invokedMethod.isIdenticalTo(bundleGetSerializable2)
+        || invokedMethod.isIdenticalTo(parcelReadSerializable1)
+        || invokedMethod.isIdenticalTo(parcelReadSerializable2)
+        || invokedMethod.isIdenticalTo(intentGetSerializableExtra1)
+        || invokedMethod.isIdenticalTo(intentGetSerializableExtra2)
+        || invokedMethod.isIdenticalTo(objectInputStreamReadObject)) {
+      pendingReflectiveUses.add(context);
+    }
+  }
+
+  @Override
+  public void notifyFixpoint(
+      Enqueuer enqueuer,
+      EnqueuerWorklist worklist,
+      ExecutorService executorService,
+      Timing timing) {
+    if (!pendingReflectiveUses.isEmpty()) {
+      timing.begin("Handle reflective Enum operations");
+      pendingReflectiveUses.forEach(this::handleReflectiveBehavior);
+      pendingReflectiveUses.clear();
+      timing.end();
+    }
+  }
+}
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 ef25826..dd20685 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -770,6 +770,8 @@
       System.getProperty("com.android.tools.r8.ignoreBootClasspathEnumsForMaindexTracing") != null;
   public boolean pruneNonVissibleAnnotationClasses =
       System.getProperty("com.android.tools.r8.pruneNonVissibleAnnotationClasses") != null;
+  public boolean experimentalTraceEnumReflection =
+      System.getProperty("com.android.tools.r8.experimentalTraceEnumReflection") != null;
 
   // Flag to turn on/offLoad/store optimization in the Cf back-end.
   public boolean enableLoadStoreOptimization = true;
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/FailingMethodEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/FailingMethodEnumUnboxingTest.java
index 01a5e94..dfd0f1a 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/FailingMethodEnumUnboxingTest.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/FailingMethodEnumUnboxingTest.java
@@ -51,7 +51,14 @@
             .addInnerClasses(FailingMethodEnumUnboxingTest.class)
             .addKeepMainRules(TESTS)
             .addKeepRules(enumKeepRules.getKeepRules())
-            .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
+            .addOptionsModification(
+                opt -> {
+                  enableEnumOptions(opt, enumValueOptimization);
+                  if (enumKeepRules == EnumKeepRules.NONE) {
+                    // Look for Enum.valueOf() when tracing rather than rely on -keeps.
+                    opt.experimentalTraceEnumReflection = true;
+                  }
+                })
             .addEnumUnboxingInspector(
                 inspector ->
                     inspector.assertNotUnboxed(
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/ValueOfEnumUnboxingFailureTest.java b/src/test/java/com/android/tools/r8/enumunboxing/ValueOfEnumUnboxingFailureTest.java
index e75d109..4662a90 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/ValueOfEnumUnboxingFailureTest.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/ValueOfEnumUnboxingFailureTest.java
@@ -39,7 +39,14 @@
         .addEnumUnboxingInspector(inspector -> inspector.assertNotUnboxed(Main.Enum.class))
         .enableNeverClassInliningAnnotations()
         .addKeepRules(enumKeepRules.getKeepRules())
-        .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
+        .addOptionsModification(
+            opt -> {
+              enableEnumOptions(opt, enumValueOptimization);
+              if (enumKeepRules == EnumKeepRules.NONE) {
+                // Look for Enum.valueOf() when tracing rather than rely on -keeps.
+                opt.experimentalTraceEnumReflection = true;
+              }
+            })
         .setMinApi(parameters)
         .compile()
         .run(parameters.getRuntime(), success)
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/ValueOfEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/ValueOfEnumUnboxingTest.java
index f976adb..e73cda0 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/ValueOfEnumUnboxingTest.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/ValueOfEnumUnboxingTest.java
@@ -46,7 +46,14 @@
                 inspector -> inspector.assertUnboxed(EnumValueOf.MyEnum.class))
             .enableNeverClassInliningAnnotations()
             .addKeepRules(enumKeepRules.getKeepRules())
-            .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
+            .addOptionsModification(
+                opt -> {
+                  enableEnumOptions(opt, enumValueOptimization);
+                  if (enumKeepRules == EnumKeepRules.NONE) {
+                    // Look for Enum.valueOf() when tracing rather than rely on -keeps.
+                    opt.experimentalTraceEnumReflection = true;
+                  }
+                })
             .setMinApi(parameters)
             .compile();
     for (Class<?> main : TESTS) {
diff --git a/src/test/java/com/android/tools/r8/naming/EnumMinification.java b/src/test/java/com/android/tools/r8/naming/EnumMinification.java
index 5b1d6f5..c72b68e 100644
--- a/src/test/java/com/android/tools/r8/naming/EnumMinification.java
+++ b/src/test/java/com/android/tools/r8/naming/EnumMinification.java
@@ -17,6 +17,7 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.enumunboxing.EnumUnboxingTestBase.EnumKeepRules;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import org.junit.Test;
@@ -50,6 +51,7 @@
         .addProgramClassFileData(enumClassFile)
         .addKeepMainRule(mainClass)
         .addKeepRules("-neverinline enum * extends java.lang.Enum { valueOf(...); }")
+        .addKeepRules(EnumKeepRules.STUDIO.getKeepRules())
         .enableProguardTestOptions()
         .setMinApi(parameters)
         .compile();
diff --git a/src/test/java/com/android/tools/r8/rewrite/enums/EnumValueOfOptimizationTest.java b/src/test/java/com/android/tools/r8/rewrite/enums/EnumValueOfOptimizationTest.java
index fe575ed..117154a 100644
--- a/src/test/java/com/android/tools/r8/rewrite/enums/EnumValueOfOptimizationTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/enums/EnumValueOfOptimizationTest.java
@@ -49,6 +49,13 @@
         .addInnerClasses(EnumValueOfOptimizationTest.class)
         .addKeepMainRule(Main.class)
         .addKeepRules(enumKeepRules.getKeepRules())
+        .addOptionsModification(
+            opt -> {
+              if (enumKeepRules == EnumKeepRules.NONE) {
+                // Look for Enum.valueOf() when tracing rather than rely on -keeps.
+                opt.experimentalTraceEnumReflection = true;
+              }
+            })
         .applyIf(
             enableNoVerticalClassMergingAnnotations,
             R8TestBuilder::enableNoVerticalClassMergingAnnotations,
diff --git a/src/test/java/com/android/tools/r8/shaking/enums/EnumCollectionsTest.java b/src/test/java/com/android/tools/r8/shaking/enums/EnumCollectionsTest.java
deleted file mode 100644
index 2a9872c..0000000
--- a/src/test/java/com/android/tools/r8/shaking/enums/EnumCollectionsTest.java
+++ /dev/null
@@ -1,208 +0,0 @@
-// Copyright (c) 2024, 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.shaking.enums;
-
-import com.android.tools.r8.NeverInline;
-import com.android.tools.r8.TestBase;
-import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
-import java.util.Arrays;
-import java.util.EnumMap;
-import java.util.EnumSet;
-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 EnumCollectionsTest extends TestBase {
-
-  @Parameter(0)
-  public TestParameters parameters;
-
-  @Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return getTestParameters().withDefaultRuntimes().withMaximumApiLevel().build();
-  }
-
-  private static final List<String> EXPECTED_OUTPUT =
-      Arrays.asList(
-          "none: [A, B]",
-          "all: [B, C]",
-          "of: [C]",
-          "of: [D, E]",
-          "of: [E, F, G]",
-          "of: [F, G, H, I]",
-          "of: [G, H, I, J, K]",
-          "of: [H, I, J, K, L, M]",
-          "range: [I, J]",
-          "map: {J=1}",
-          "valueOf: K",
-          "phi: [B]");
-
-  public static class TestMain {
-    public enum EnumA {
-      A,
-      B
-    }
-
-    public enum EnumB {
-      B,
-      C
-    }
-
-    public enum EnumC {
-      C,
-      D
-    }
-
-    public enum EnumD {
-      D,
-      E
-    }
-
-    public enum EnumE {
-      E,
-      F,
-      G
-    }
-
-    public enum EnumF {
-      F,
-      G,
-      H,
-      I
-    }
-
-    public enum EnumG {
-      G,
-      H,
-      I,
-      J,
-      K
-    }
-
-    public enum EnumH {
-      H,
-      I,
-      J,
-      K,
-      L,
-      M
-    }
-
-    public enum EnumI {
-      I,
-      J
-    }
-
-    public enum EnumJ {
-      J,
-      K
-    }
-
-    public enum EnumK {
-      K,
-      L
-    }
-
-    @NeverInline
-    private static void noneOf() {
-      System.out.println("none: " + EnumSet.complementOf(EnumSet.noneOf(EnumA.class)));
-    }
-
-    @NeverInline
-    private static void allOf() {
-      System.out.println("all: " + EnumSet.allOf(EnumB.class));
-    }
-
-    @NeverInline
-    private static void of1() {
-      System.out.println("of: " + EnumSet.of(EnumC.C));
-    }
-
-    @NeverInline
-    private static void of2() {
-      System.out.println("of: " + EnumSet.of(EnumD.D, EnumD.E));
-    }
-
-    @NeverInline
-    private static void of3() {
-      System.out.println("of: " + EnumSet.of(EnumE.E, EnumE.F, EnumE.G));
-    }
-
-    @NeverInline
-    private static void of4() {
-      System.out.println("of: " + EnumSet.of(EnumF.F, EnumF.G, EnumF.H, EnumF.I));
-    }
-
-    @NeverInline
-    private static void of5() {
-      System.out.println("of: " + EnumSet.of(EnumG.G, EnumG.H, EnumG.I, EnumG.J, EnumG.K));
-    }
-
-    @NeverInline
-    private static void ofVarArgs() {
-      System.out.println("of: " + EnumSet.of(EnumH.H, EnumH.I, EnumH.J, EnumH.K, EnumH.L, EnumH.M));
-    }
-
-    @NeverInline
-    private static void range() {
-      System.out.println("range: " + EnumSet.range(EnumI.I, EnumI.J));
-    }
-
-    @NeverInline
-    private static void map() {
-      EnumMap<EnumJ, Integer> map = new EnumMap<>(EnumJ.class);
-      map.put(EnumJ.J, 1);
-      System.out.println("map: " + map);
-    }
-
-    @NeverInline
-    private static void valueOf() {
-      System.out.println("valueOf: " + EnumK.valueOf("K"));
-    }
-
-    public static void main(String[] args) {
-      // Use different methods to ensure Enqueuer.traceInvokeStatic() triggers for each one.
-      noneOf();
-      allOf();
-      of1();
-      of2();
-      of3();
-      of4();
-      of5();
-      ofVarArgs();
-      range();
-      map();
-      valueOf();
-      // Ensure phi as argument does not cause issues.
-      System.out.println(
-          "phi: " + EnumSet.of((Enum) (args.length > 10 ? (Object) EnumA.A : (Object) EnumB.B)));
-    }
-  }
-
-  @Test
-  public void testRuntime() throws Exception {
-    testForRuntime(parameters)
-        .addProgramClassesAndInnerClasses(TestMain.class)
-        .run(parameters.getRuntime(), TestMain.class)
-        .assertSuccessWithOutputLines(EXPECTED_OUTPUT);
-  }
-
-  @Test
-  public void testR8() throws Exception {
-    testForR8(parameters.getBackend())
-        .setMinApi(parameters)
-        .addProgramClassesAndInnerClasses(TestMain.class)
-        .enableInliningAnnotations()
-        .addKeepMainRule(TestMain.class)
-        .compile()
-        .run(parameters.getRuntime(), TestMain.class)
-        .assertSuccessWithOutputLines(EXPECTED_OUTPUT);
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/shaking/enums/EnumReflectionTest.java b/src/test/java/com/android/tools/r8/shaking/enums/EnumReflectionTest.java
new file mode 100644
index 0000000..9e9b297
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/enums/EnumReflectionTest.java
@@ -0,0 +1,505 @@
+// Copyright (c) 2025, 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.shaking.enums;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.shaking.enums.EnumReflectionTest.Helpers.EnumA;
+import com.android.tools.r8.shaking.enums.EnumReflectionTest.Helpers.EnumB;
+import com.android.tools.r8.shaking.enums.EnumReflectionTest.Helpers.EnumC;
+import com.android.tools.r8.shaking.enums.EnumReflectionTest.Helpers.EnumD;
+import com.android.tools.r8.shaking.enums.EnumReflectionTest.Helpers.EnumE;
+import com.android.tools.r8.shaking.enums.EnumReflectionTest.Helpers.EnumF;
+import com.android.tools.r8.shaking.enums.EnumReflectionTest.Helpers.EnumG;
+import com.android.tools.r8.shaking.enums.EnumReflectionTest.Helpers.EnumH;
+import com.android.tools.r8.shaking.enums.EnumReflectionTest.Helpers.EnumI;
+import com.android.tools.r8.shaking.enums.EnumReflectionTest.Helpers.EnumJ;
+import com.android.tools.r8.shaking.enums.EnumReflectionTest.Helpers.EnumK;
+import com.android.tools.r8.shaking.enums.EnumReflectionTest.Helpers.EnumL;
+import com.android.tools.r8.shaking.enums.EnumReflectionTest.Helpers.EnumM;
+import com.android.tools.r8.shaking.enums.EnumReflectionTest.Helpers.EnumN;
+import com.android.tools.r8.shaking.enums.EnumReflectionTest.Helpers.EnumO;
+import com.android.tools.r8.shaking.enums.EnumReflectionTest.Helpers.EnumP;
+import com.android.tools.r8.shaking.enums.EnumReflectionTest.Helpers.EnumQ;
+import com.android.tools.r8.shaking.enums.EnumReflectionTest.Helpers.EnumR;
+import com.android.tools.r8.shaking.enums.EnumReflectionTest.Helpers.EnumS;
+import com.android.tools.r8.shaking.enums.EnumReflectionTest.Helpers.EnumT;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.lang.reflect.Array;
+import java.util.Arrays;
+import java.util.EnumMap;
+import java.util.EnumSet;
+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;
+
+/** Tests for the various APIs that cause an Enum type's values() method to be kept. */
+@RunWith(Parameterized.class)
+public class EnumReflectionTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultRuntimes().withMaximumApiLevel().build();
+  }
+
+  public static class FakeParcel {
+    // Deserializing arrays yields UnsatisfiedLinkError for com.android.org.conscrypt.NativeCrypto
+    // when running under ART.
+    private Class arrayType;
+    private ObjectInputStream objectInputStream;
+
+    public static byte[] toBytes(Serializable thing) {
+      try {
+        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
+        objectOutputStream.writeObject(thing);
+        return byteArrayOutputStream.toByteArray();
+      } catch (IOException e) {
+        throw new RuntimeException(e);
+      }
+    }
+
+    public static FakeParcel createWithSingleSerializable(Serializable thing) {
+      try {
+        FakeParcel ret = new FakeParcel();
+        if (thing.getClass().isArray()) {
+          ret.arrayType = thing.getClass();
+          if (Array.getLength(thing) != 1) {
+            throw new IllegalArgumentException();
+          }
+          thing = (Serializable) Array.get(thing, 0);
+        }
+        ret.objectInputStream = new ObjectInputStream(new ByteArrayInputStream(toBytes(thing)));
+        return ret;
+      } catch (IOException e) {
+        throw new RuntimeException(e);
+      }
+    }
+
+    public Serializable readSerializable() {
+      try {
+        Serializable ret = (Serializable) objectInputStream.readObject();
+        if (arrayType != null) {
+          Object array = Array.newInstance(arrayType.getComponentType(), 1);
+          Array.set(array, 0, ret);
+          ret = (Serializable) array;
+        }
+        return ret;
+      } catch (Exception e) {
+        throw new RuntimeException(e);
+      }
+    }
+
+    public <T extends Serializable> T readSerializable(ClassLoader loader, Class<T> clazz) {
+      return clazz.cast(readSerializable());
+    }
+  }
+
+  public static class FakeBundle {
+    private Object parcel;
+
+    public static FakeBundle createWithSingleSerializable(Serializable thing) {
+      FakeBundle ret = new FakeBundle();
+      ret.parcel = FakeParcel.createWithSingleSerializable(thing);
+      return ret;
+    }
+
+    public <T extends Serializable> T getSerializable(String key, Class<T> clazz) {
+      return clazz.cast(getSerializable(key));
+    }
+
+    public Serializable getSerializable(String key) {
+      return ((FakeParcel) parcel).readSerializable();
+    }
+  }
+
+  public static class FakeIntent {
+    private Object parcel;
+
+    public static FakeIntent createWithSingleSerializable(Serializable thing) {
+      FakeIntent ret = new FakeIntent();
+      ret.parcel = FakeParcel.createWithSingleSerializable(thing);
+      return ret;
+    }
+
+    public <T extends Serializable> T getSerializableExtra(String key, Class<T> clazz) {
+      return clazz.cast(getSerializableExtra(key));
+    }
+
+    public Serializable getSerializableExtra(String key) {
+      return ((FakeParcel) parcel).readSerializable();
+    }
+  }
+
+  private static final List<String> EXPECTED_OUTPUT =
+      Arrays.asList(
+          "none: [A, B]",
+          "all: [B, C]",
+          "of: [C]",
+          "of: [D, E]",
+          "of: [E, F, G]",
+          "of: [F, G, H, I]",
+          "of: [G, H, I, J, K]",
+          "of: [H, I, J, K, L, M]",
+          "range: [I, J]",
+          "map: {J=1}",
+          "valueOf: K",
+          "bundle: L",
+          "bundle: M",
+          "parcel: N",
+          "parcel: O",
+          "intent: P",
+          "intent: Q",
+          "stream: R",
+          "array: [S]",
+          "array: [T]",
+          "phi: [B]");
+
+  public static class Helpers {
+
+    public enum EnumA {
+      A,
+      B
+    }
+
+    public enum EnumB {
+      B,
+      C
+    }
+
+    public enum EnumC {
+      C,
+      D
+    }
+
+    public enum EnumD {
+      D,
+      E
+    }
+
+    public enum EnumE {
+      E,
+      F,
+      G
+    }
+
+    public enum EnumF {
+      F,
+      G,
+      H,
+      I
+    }
+
+    public enum EnumG {
+      G,
+      H,
+      I,
+      J,
+      K
+    }
+
+    public enum EnumH {
+      H,
+      I,
+      J,
+      K,
+      L,
+      M
+    }
+
+    public enum EnumI {
+      I,
+      J
+    }
+
+    public enum EnumJ {
+      J,
+      K
+    }
+
+    public enum EnumK {
+      K,
+      L
+    }
+
+    public enum EnumL {
+      L,
+      M
+    }
+
+    public enum EnumM {
+      M,
+      N
+    }
+
+    public enum EnumN {
+      N,
+      O
+    }
+
+    public enum EnumO {
+      O,
+      P
+    }
+
+    public enum EnumP {
+      P,
+      Q
+    }
+
+    public enum EnumQ {
+      Q,
+      R
+    }
+
+    public enum EnumR {
+      R {}, // Test anonymous enum subtype.
+      S
+    }
+
+    public enum EnumS {
+      S,
+      T
+    }
+
+    public enum EnumT {
+      T,
+      U
+    }
+  }
+
+  public static class TestMain {
+    @NeverInline
+    private static void noneOf() {
+      System.out.println("none: " + EnumSet.complementOf(EnumSet.noneOf(EnumA.class)));
+    }
+
+    @NeverInline
+    private static void allOf() {
+      System.out.println("all: " + EnumSet.allOf(EnumB.class));
+    }
+
+    @NeverInline
+    private static void of1() {
+      System.out.println("of: " + EnumSet.of(EnumC.C));
+    }
+
+    @NeverInline
+    private static void of2() {
+      System.out.println("of: " + EnumSet.of(EnumD.D, EnumD.E));
+    }
+
+    @NeverInline
+    private static void of3() {
+      System.out.println("of: " + EnumSet.of(EnumE.E, EnumE.F, EnumE.G));
+    }
+
+    @NeverInline
+    private static void of4() {
+      System.out.println("of: " + EnumSet.of(EnumF.F, EnumF.G, EnumF.H, EnumF.I));
+    }
+
+    @NeverInline
+    private static void of5() {
+      System.out.println("of: " + EnumSet.of(EnumG.G, EnumG.H, EnumG.I, EnumG.J, EnumG.K));
+    }
+
+    @NeverInline
+    private static void ofVarArgs() {
+      System.out.println("of: " + EnumSet.of(EnumH.H, EnumH.I, EnumH.J, EnumH.K, EnumH.L, EnumH.M));
+    }
+
+    @NeverInline
+    private static void range() {
+      System.out.println("range: " + EnumSet.range(EnumI.I, EnumI.J));
+    }
+
+    @NeverInline
+    private static void map() {
+      EnumMap<EnumJ, Integer> map = new EnumMap<>(EnumJ.class);
+      map.put(EnumJ.J, 1);
+      System.out.println("map: " + map);
+    }
+
+    @NeverInline
+    private static void valueOf() {
+      System.out.println("valueOf: " + EnumK.valueOf("K"));
+    }
+
+    @NeverInline
+    private static void androidBundle1() {
+      FakeBundle b = FakeBundle.createWithSingleSerializable(EnumL.L);
+      EnumL result = (EnumL) b.getSerializable("");
+      System.out.println("bundle: " + result);
+    }
+
+    @NeverInline
+    private static void androidBundle2() {
+      FakeBundle b = FakeBundle.createWithSingleSerializable(EnumM.M);
+      EnumM result = b.getSerializable("", EnumM.class);
+      System.out.println("bundle: " + result);
+    }
+
+    @NeverInline
+    private static void androidParcel1() {
+      FakeParcel p = FakeParcel.createWithSingleSerializable(EnumN.N);
+      EnumN result = (EnumN) p.readSerializable();
+      System.out.println("parcel: " + result);
+    }
+
+    @NeverInline
+    private static void androidParcel2() {
+      FakeParcel p = FakeParcel.createWithSingleSerializable(EnumO.O);
+      System.out.println("parcel: " + p.readSerializable(null, EnumO.class));
+    }
+
+    @NeverInline
+    private static void androidIntent1() {
+      FakeIntent i = FakeIntent.createWithSingleSerializable(EnumP.P);
+      EnumP result = (EnumP) i.getSerializableExtra("");
+      System.out.println("intent: " + result);
+    }
+
+    @NeverInline
+    private static void androidIntent2() {
+      FakeIntent i = FakeIntent.createWithSingleSerializable(EnumQ.Q);
+      System.out.println("intent: " + i.getSerializableExtra("", EnumQ.class));
+    }
+
+    @NeverInline
+    private static void array1() {
+      FakeParcel p = FakeParcel.createWithSingleSerializable(new EnumS[] {EnumS.S});
+      EnumS[] result = (EnumS[]) p.readSerializable();
+      System.out.println("array: " + Arrays.toString(result));
+    }
+
+    @NeverInline
+    private static void array2() {
+      FakeParcel p = FakeParcel.createWithSingleSerializable(new EnumT[] {EnumT.T});
+      System.out.println("array: " + Arrays.toString(p.readSerializable(null, EnumT[].class)));
+    }
+
+    @NeverInline
+    private static void objectStream() {
+      try {
+        ObjectInputStream objectInputStream =
+            new ObjectInputStream(new ByteArrayInputStream(FakeParcel.toBytes(EnumR.R)));
+        EnumR result = (EnumR) objectInputStream.readObject();
+        System.out.println("stream: " + result);
+      } catch (Exception e) {
+        throw new RuntimeException(e);
+      }
+    }
+
+    public static void main(String[] args) {
+      // Use different methods to ensure Enqueuer.traceInvokeStatic() triggers for each one.
+      noneOf();
+      allOf();
+      of1();
+      of2();
+      of3();
+      of4();
+      of5();
+      ofVarArgs();
+      range();
+      map();
+      valueOf();
+      androidBundle1();
+      androidBundle2();
+      androidParcel1();
+      androidParcel2();
+      androidIntent1();
+      androidIntent2();
+      objectStream();
+      array1();
+      array2();
+      // Ensure phi as argument does not cause issues.
+      System.out.println(
+          "phi: " + EnumSet.of((Enum) (args.length > 10 ? (Object) EnumA.A : (Object) EnumB.B)));
+    }
+  }
+
+  // EnumR.R references this class in its constructor.
+  private static final String ENUM_SUBTYPE_BRIDGE_CLASS_NAME =
+      EnumReflectionTest.class.getName() + "$1";
+
+  private static final String PARCEL_DESCRIPTOR = "Landroid/os/Parcel;";
+  private static final String BUNDLE_DESCRIPTOR = "Landroid/os/Bundle;";
+  private static final String INTENT_DESCRIPTOR = "Landroid/content/Intent;";
+
+  private static byte[] rewriteTestMain() throws IOException {
+    return transformer(TestMain.class)
+        .replaceClassDescriptorInMethodInstructions(descriptor(FakeParcel.class), PARCEL_DESCRIPTOR)
+        .replaceClassDescriptorInMethodInstructions(descriptor(FakeBundle.class), BUNDLE_DESCRIPTOR)
+        .replaceClassDescriptorInMethodInstructions(descriptor(FakeIntent.class), INTENT_DESCRIPTOR)
+        .transform();
+  }
+
+  private static byte[] rewriteParcel() throws IOException {
+    return transformer(FakeParcel.class).setClassDescriptor(PARCEL_DESCRIPTOR).transform();
+  }
+
+  private static byte[] rewriteBundle() throws IOException {
+    return transformer(FakeBundle.class)
+        .setClassDescriptor(BUNDLE_DESCRIPTOR)
+        .replaceClassDescriptorInMethodInstructions(descriptor(FakeParcel.class), PARCEL_DESCRIPTOR)
+        .transform();
+  }
+
+  private static byte[] rewriteIntent() throws IOException {
+    return transformer(FakeIntent.class)
+        .setClassDescriptor(INTENT_DESCRIPTOR)
+        .replaceClassDescriptorInMethodInstructions(descriptor(FakeParcel.class), PARCEL_DESCRIPTOR)
+        .transform();
+  }
+
+  @Test
+  public void testRuntime() throws Exception {
+    byte[] parcelBytes = rewriteParcel();
+    byte[] bundleBytes = rewriteBundle();
+    byte[] intentBytes = rewriteIntent();
+    testForRuntime(parameters)
+        .addProgramClassesAndInnerClasses(Helpers.class)
+        .addProgramClassFileData(rewriteTestMain())
+        .addProgramClasses(Class.forName(ENUM_SUBTYPE_BRIDGE_CLASS_NAME))
+        .addClasspathClassFileData(parcelBytes, bundleBytes, intentBytes)
+        .addRunClasspathFiles(buildOnDexRuntime(parameters, parcelBytes, bundleBytes, intentBytes))
+        .run(parameters.getRuntime(), TestMain.class)
+        .assertSuccessWithOutputLines(EXPECTED_OUTPUT);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    byte[] parcelBytes = rewriteParcel();
+    byte[] bundleBytes = rewriteBundle();
+    byte[] intentBytes = rewriteIntent();
+    testForR8(parameters.getBackend())
+        .setMinApi(parameters)
+        .addOptionsModification(options -> options.experimentalTraceEnumReflection = true)
+        .addProgramClassesAndInnerClasses(Helpers.class)
+        .addProgramClassFileData(rewriteTestMain())
+        .addProgramClasses(Class.forName(ENUM_SUBTYPE_BRIDGE_CLASS_NAME))
+        .addClasspathClassFileData(parcelBytes, bundleBytes, intentBytes)
+        .enableInliningAnnotations()
+        .addKeepMainRule(TestMain.class)
+        .compile()
+        .addRunClasspathClassFileData(parcelBytes, bundleBytes, intentBytes)
+        .run(parameters.getRuntime(), TestMain.class)
+        .assertSuccessWithOutputLines(EXPECTED_OUTPUT);
+  }
+}