Add experimental option to trace enum usage in Android serialization APIs

Enabled by system property:
  com.android.tools.r8.experimentalTraceAndroidEnumSerialization

The APIs that are looked for:
 * Bundle.getSerializable()
 * Parcel.readSerializable()
 * Intent.getSerializableExtra()

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.

Reorganizes enum tracing into two helper classes:
 * EnqueuerEnumReflectionSupportAndroid
 * EnqueuerEnumReflectionSupportJava

Adds support for tracing to recognize enum subclasses.

Bug: b/204939965
Change-Id: I7db7a73a63e611fd0465a0c6888f6416b4c1ef05
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..1dda2a0 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -402,9 +402,6 @@
   // Method names used on MethodHandles.
   public final DexString lookupString = createString("lookup");
   public final DexString privateLookupInString = createString("privateLookupIn");
-  public final DexString mockString = createString("mock");
-  public final DexString spyString = createString("spy");
-
   public final DexType booleanType = createStaticallyKnownType(booleanDescriptor);
   public final DexType byteType = createStaticallyKnownType(byteDescriptor);
   public final DexType charType = createStaticallyKnownType(charDescriptor);
@@ -640,9 +637,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 +758,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(
@@ -896,8 +888,6 @@
       createStaticallyKnownType(desugarVarHandleDescriptorString);
   public final DexType desugarMethodHandlesLookupType =
       createStaticallyKnownType(desugarMethodHandlesLookupDescriptorString);
-  public final DexType mockitoType = createStaticallyKnownType("Lorg/mockito/Mockito;");
-
   public final DexType javaUtilConcurrentExecutorServiceType =
       createStaticallyKnownType("Ljava/util/concurrent/ExecutorService;");
   public final DexType javaUtilConcurrentForkJoinPoolType =
@@ -1614,28 +1604,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/graph/analysis/EnqueuerAnalysisCollection.java b/src/main/java/com/android/tools/r8/graph/analysis/EnqueuerAnalysisCollection.java
index babfa95..f1320a4 100644
--- a/src/main/java/com/android/tools/r8/graph/analysis/EnqueuerAnalysisCollection.java
+++ b/src/main/java/com/android/tools/r8/graph/analysis/EnqueuerAnalysisCollection.java
@@ -17,6 +17,7 @@
 import com.android.tools.r8.graph.ProgramDefinition;
 import com.android.tools.r8.graph.ProgramField;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.shaking.DefaultEnqueuerUseRegistry;
 import com.android.tools.r8.shaking.Enqueuer;
 import com.android.tools.r8.shaking.Enqueuer.FieldAccessKind;
@@ -39,6 +40,9 @@
   private final TraceInvokeEnqueuerAnalysis[] invokeAnalyses;
   private final TraceNewInstanceEnqueuerAnalysis[] newInstanceAnalyses;
 
+  // IR analysis.
+  private final IrBasedEnqueuerAnalysis[] irBasedEnqueuerAnalyses;
+
   // Reachability events.
   private final NewlyFailedMethodResolutionEnqueuerAnalysis[] newlyFailedMethodResolutionAnalyses;
   private final NewlyLiveClassEnqueuerAnalysis[] newlyLiveClassAnalyses;
@@ -64,6 +68,8 @@
       TraceInstanceOfEnqueuerAnalysis[] instanceOfAnalyses,
       TraceInvokeEnqueuerAnalysis[] invokeAnalyses,
       TraceNewInstanceEnqueuerAnalysis[] newInstanceAnalyses,
+      // IR analysis.
+      IrBasedEnqueuerAnalysis[] irBasedEnqueuerAnalyses,
       // Reachability events.
       NewlyFailedMethodResolutionEnqueuerAnalysis[] newlyFailedMethodResolutionAnalyses,
       NewlyLiveClassEnqueuerAnalysis[] newlyLiveClassAnalyses,
@@ -86,6 +92,8 @@
     this.instanceOfAnalyses = instanceOfAnalyses;
     this.invokeAnalyses = invokeAnalyses;
     this.newInstanceAnalyses = newInstanceAnalyses;
+    // IR Analysis.
+    this.irBasedEnqueuerAnalyses = irBasedEnqueuerAnalyses;
     // Reachability events.
     this.newlyFailedMethodResolutionAnalyses = newlyFailedMethodResolutionAnalyses;
     this.newlyLiveClassAnalyses = newlyLiveClassAnalyses;
@@ -357,6 +365,15 @@
     }
   }
 
+  public boolean handleReflectiveInvoke(ProgramMethod method, InvokeMethod invoke) {
+    for (IrBasedEnqueuerAnalysis analysis : irBasedEnqueuerAnalyses) {
+      if (analysis.handleReflectiveInvoke(method, invoke)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
   public static class Builder {
 
     // Trace events.
@@ -369,6 +386,9 @@
     private final List<TraceInvokeEnqueuerAnalysis> invokeAnalyses = new ArrayList<>();
     private final List<TraceNewInstanceEnqueuerAnalysis> newInstanceAnalyses = new ArrayList<>();
 
+    // IR analysis.
+    private final List<IrBasedEnqueuerAnalysis> irBasedEnqueuerAnalyses = new ArrayList<>();
+
     // Reachability events.
     private final List<NewlyFailedMethodResolutionEnqueuerAnalysis>
         newlyFailedMethodResolutionAnalyses = new ArrayList<>();
@@ -430,6 +450,12 @@
       return this;
     }
 
+    // IR analysis.
+    public Builder addIrBasedEnqueuerAnalysis(IrBasedEnqueuerAnalysis analysis) {
+      irBasedEnqueuerAnalyses.add(analysis);
+      return this;
+    }
+
     // Reachability events.
 
     public Builder addNewlyFailedMethodResolutionAnalysis(
@@ -507,6 +533,8 @@
           instanceOfAnalyses.toArray(TraceInstanceOfEnqueuerAnalysis[]::new),
           invokeAnalyses.toArray(TraceInvokeEnqueuerAnalysis[]::new),
           newInstanceAnalyses.toArray(TraceNewInstanceEnqueuerAnalysis[]::new),
+          // IR analysis.
+          irBasedEnqueuerAnalyses.toArray(IrBasedEnqueuerAnalysis[]::new),
           // Reachability events.
           newlyFailedMethodResolutionAnalyses.toArray(
               NewlyFailedMethodResolutionEnqueuerAnalysis[]::new),
diff --git a/src/main/java/com/android/tools/r8/graph/analysis/IrBasedEnqueuerAnalysis.java b/src/main/java/com/android/tools/r8/graph/analysis/IrBasedEnqueuerAnalysis.java
new file mode 100644
index 0000000..4940118
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/analysis/IrBasedEnqueuerAnalysis.java
@@ -0,0 +1,13 @@
+// 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.graph.analysis;
+
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.code.InvokeMethod;
+
+public interface IrBasedEnqueuerAnalysis {
+
+  /** Called for methods added by Enqueuer.addMethodThatRequireIrAnalysis(). */
+  boolean handleReflectiveInvoke(ProgramMethod method, InvokeMethod invoke);
+}
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..626bc31 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;
@@ -398,7 +396,7 @@
   private final ProguardCompatibilityActions.Builder proguardCompatibilityActionsBuilder;
 
   /** A set of methods that need code inspection for Java reflection in use. */
-  private final ProgramMethodSet pendingReflectiveUses = ProgramMethodSet.createLinked();
+  private final ProgramMethodSet methodsThatRequireIrAnalysis = ProgramMethodSet.create();
 
   /** Mapping of types to the resolved methods for that type along with the context. */
   private final Map<DexProgramClass, Map<ResolutionSearchKey, ProgramMethodSet>>
@@ -553,6 +551,14 @@
       ProtoEnqueuerExtension.register(appView, analysesBuilder);
       ResourceAccessAnalysis.register(appView, this, analysesBuilder);
       RuntimeTypeCheckInfo.register(runtimeTypeCheckInfoBuilder, analysesBuilder);
+      EnqueuerMockitoAnalysis.register(appView, this, analysesBuilder);
+      // Enum reflection tracing is best-effort, but since it is more common for non-Android uses to
+      // omit the Enum.values() -keep that Android Studio uses, we enable Java enum reflection
+      // tracing by default, but leave Android's behind a flag.
+      new EnqueuerEnumReflectionAnalysisJava(appView, this).register(analysesBuilder);
+      if (options.experimentalTraceAndroidEnumSerialization) {
+        new EnqueuerEnumReflectionAnalysisAndroid(appView, this).register(analysesBuilder);
+      }
     }
     analyses = analysesBuilder.build();
 
@@ -1548,11 +1554,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(
@@ -1606,18 +1607,12 @@
       // 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.
-      pendingReflectiveUses.add(context);
-    } else if (invokedMethod == dexItemFactory.enumMembers.valueOf
-        || dexItemFactory.javaUtilEnumSetMembers.isFactoryMethod(invokedMethod)) {
-      // See comment in handleEnumValueOfOrCollectionInstantiation.
-      pendingReflectiveUses.add(context);
+      addMethodThatRequireIrAnalysis(context);
     } else if (invokedMethod == dexItemFactory.proxyMethods.newProxyInstance) {
-      pendingReflectiveUses.add(context);
+      addMethodThatRequireIrAnalysis(context);
     } else if (dexItemFactory.serviceLoaderMethods.isLoadMethod(invokedMethod)) {
       // Handling of application services.
-      pendingReflectiveUses.add(context);
-    } else if (EnqueuerMockitoSupport.isReflectiveMockInvoke(dexItemFactory, invokedMethod)) {
-      pendingReflectiveUses.add(context);
+      addMethodThatRequireIrAnalysis(context);
     }
     markTypeAsLive(invokedMethod.getHolderType(), context);
     MethodResolutionResult resolutionResult =
@@ -1647,7 +1642,6 @@
         invokedMethod, context, registry, KeepReason.invokedFromLambdaCreatedIn(context));
   }
 
-  @SuppressWarnings("ReferenceEquality")
   private void traceInvokeVirtual(
       DexMethod invokedMethod,
       ProgramMethod context,
@@ -1656,14 +1650,15 @@
     if (registry != null && !registry.markInvokeVirtualAsSeen(invokedMethod)) {
       return;
     }
-    if (invokedMethod == appView.dexItemFactory().classMethods.newInstance
-        || invokedMethod == appView.dexItemFactory().constructorMethods.newInstance) {
-      pendingReflectiveUses.add(context);
-    } else if (appView.dexItemFactory().classMethods.isReflectiveMemberLookup(invokedMethod)) {
+    DexItemFactory dexItemFactory = appView.dexItemFactory();
+    if (invokedMethod.isIdenticalTo(dexItemFactory.classMethods.newInstance)
+        || invokedMethod.isIdenticalTo(dexItemFactory.constructorMethods.newInstance)) {
+      addMethodThatRequireIrAnalysis(context);
+    } 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.
-      pendingReflectiveUses.add(context);
+      addMethodThatRequireIrAnalysis(context);
     }
     markTypeAsLive(invokedMethod.getHolderType(), context);
     MethodResolutionResult resolutionResult =
@@ -3731,7 +3726,7 @@
         .createMethod(enumClass.type, proto, appView.dexItemFactory().createString("values"));
   }
 
-  private void markEnumValuesAsReachable(DexProgramClass clazz, KeepReason reason) {
+  void markEnumValuesAsReachable(DexProgramClass clazz, KeepReason reason) {
     ProgramMethod valuesMethod = clazz.lookupProgramMethod(generatedEnumValuesMethod(clazz));
     if (valuesMethod != null) {
       // TODO(sgjesse): Does this have to be enqueued as a root item? Right now it is done as the
@@ -4750,10 +4745,10 @@
 
         // Continue fix-point processing while there are additional work items to ensure items that
         // are passed to Java reflections are traced.
-        if (!pendingReflectiveUses.isEmpty()) {
+        if (!methodsThatRequireIrAnalysis.isEmpty()) {
           timing.begin("Handle reflective behavior");
-          pendingReflectiveUses.forEach(this::handleReflectiveBehavior);
-          pendingReflectiveUses.clear();
+          methodsThatRequireIrAnalysis.forEach(this::handleReflectiveBehavior);
+          methodsThatRequireIrAnalysis.clear();
           timing.end();
         }
         if (worklist.hasNext()) {
@@ -5219,38 +5214,33 @@
         method, method, graphReporter.reportCompatKeepMethod(method));
   }
 
+  public void addMethodThatRequireIrAnalysis(ProgramMethod programMethod) {
+    methodsThatRequireIrAnalysis.add(programMethod);
+  }
+
   private void handleReflectiveBehavior(ProgramMethod method) {
     IRCode code = method.buildIR(appView, MethodConversionOptions.nonConverting());
     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;
     }
@@ -5258,10 +5248,10 @@
       handleServiceLoaderInvocation(method, invoke);
       return;
     }
-    if (EnqueuerMockitoSupport.isReflectiveMockInvoke(appView.dexItemFactory(), invokedMethod)) {
-      EnqueuerMockitoSupport.handleReflectiveMockInvoke(appView, keepInfo, method, invoke);
+    if (analyses.handleReflectiveInvoke(method, invoke)) {
       return;
     }
+
     if (!isReflectionMethod(dexItemFactory, invokedMethod)) {
       return;
     }
@@ -5573,47 +5563,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/EnqueuerEnumReflectionAnalysisAndroid.java b/src/main/java/com/android/tools/r8/shaking/EnqueuerEnumReflectionAnalysisAndroid.java
new file mode 100644
index 0000000..7e7735a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/EnqueuerEnumReflectionAnalysisAndroid.java
@@ -0,0 +1,149 @@
+// 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.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+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.ir.code.CheckCast;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.Value;
+
+/**
+ * Finds Enum.values() methods that must be kept due to uses of common Android serialization APIs.
+ *
+ * <p>Looks for:
+ *
+ * <ul>
+ *   <li>Parcel.readSerializable()
+ *   <li>Bundle.getSerializable()
+ *   <li>Intent.getSerializableExtra()
+ * </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 EnqueuerEnumReflectionAnalysisAndroid extends EnqueuerEnumReflectionAnalysisBase {
+  private final DexType androidContentIntentType;
+  private final DexType androidOsParcelType;
+  private final DexMethod intentGetSerializableExtra1;
+  private final DexMethod intentGetSerializableExtra2;
+  private final DexMethod bundleGetSerializable1;
+  private final DexMethod bundleGetSerializable2;
+  private final DexMethod parcelReadSerializable1;
+  private final DexMethod parcelReadSerializable2;
+
+  public EnqueuerEnumReflectionAnalysisAndroid(
+      AppView<? extends AppInfoWithClassHierarchy> appView, Enqueuer enqueuer) {
+    super(appView, enqueuer);
+    DexItemFactory dexItemFactory = appView.dexItemFactory();
+
+    androidContentIntentType = dexItemFactory.createType("Landroid/content/Intent;");
+    androidOsParcelType = dexItemFactory.createType("Landroid/os/Parcel;");
+    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");
+  }
+
+  @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)) {
+      enqueuer.addMethodThatRequireIrAnalysis(context);
+    }
+  }
+
+  @Override
+  public boolean handleReflectiveInvoke(ProgramMethod method, InvokeMethod invoke) {
+    DexMethod invokedMethod = invoke.getInvokedMethod();
+    if (invokedMethod.isIdenticalTo(bundleGetSerializable2)
+        || invokedMethod.isIdenticalTo(parcelReadSerializable2)
+        || invokedMethod.isIdenticalTo(intentGetSerializableExtra2)) {
+      handleReflectiveEnumInvoke(method, invoke, 1);
+      return true;
+    }
+    if (invokedMethod.isIdenticalTo(bundleGetSerializable1)
+        || invokedMethod.isIdenticalTo(parcelReadSerializable1)
+        || invokedMethod.isIdenticalTo(intentGetSerializableExtra1)) {
+      handleEnumCastFromSerializable(method, invoke.outValue());
+      return true;
+    }
+    return false;
+  }
+
+  private void handleEnumCastFromSerializable(ProgramMethod context, Value value) {
+    // These forms do not take a Class parameter, but are often followed by a checked-cast to their
+    // concrete type.
+    // There is no checked-cast when the return value is used as an Object (e.g. toString(), or
+    // inserted into a List<Object>).
+    // Does not identify when the above methods are wrapped in a helper method
+    // (e.g. a "IntentUtils.safeGetSerializableExtra()").
+    // Does not find enums nested in Serializable types. E.g., and ArrayList of enums.
+    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) {
+        enqueuer.markEnumValuesAsReachable(enumClass, KeepReason.invokedFrom(context));
+      }
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/EnqueuerEnumReflectionAnalysisBase.java b/src/main/java/com/android/tools/r8/shaking/EnqueuerEnumReflectionAnalysisBase.java
new file mode 100644
index 0000000..237959c
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/EnqueuerEnumReflectionAnalysisBase.java
@@ -0,0 +1,93 @@
+// 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.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.analysis.EnqueuerAnalysisCollection;
+import com.android.tools.r8.graph.analysis.IrBasedEnqueuerAnalysis;
+import com.android.tools.r8.graph.analysis.TraceInvokeEnqueuerAnalysis;
+import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
+import com.android.tools.r8.ir.code.ConstClass;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.Value;
+
+abstract class EnqueuerEnumReflectionAnalysisBase
+    implements TraceInvokeEnqueuerAnalysis, IrBasedEnqueuerAnalysis {
+  private final AppView<? extends AppInfoWithClassHierarchy> appView;
+  protected final Enqueuer enqueuer;
+
+  public EnqueuerEnumReflectionAnalysisBase(
+      AppView<? extends AppInfoWithClassHierarchy> appView, Enqueuer enqueuer) {
+    this.appView = appView;
+    this.enqueuer = enqueuer;
+  }
+
+  public void register(EnqueuerAnalysisCollection.Builder builder) {
+    builder.addTraceInvokeAnalysis(this);
+    builder.addIrBasedEnqueuerAnalysis(this);
+  }
+
+  protected 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;
+  }
+
+  protected void handleReflectiveEnumInvoke(
+      ProgramMethod context, InvokeMethod invoke, int classParamIndex) {
+    // 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();
+    }
+    DexProgramClass clazz = maybeGetProgramEnumType(type, true);
+    if (clazz != null) {
+      enqueuer.markEnumValuesAsReachable(clazz, KeepReason.invokedFrom(context));
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/EnqueuerEnumReflectionAnalysisJava.java b/src/main/java/com/android/tools/r8/shaking/EnqueuerEnumReflectionAnalysisJava.java
new file mode 100644
index 0000000..1d528fb
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/EnqueuerEnumReflectionAnalysisJava.java
@@ -0,0 +1,102 @@
+// 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.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+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.ir.code.InvokeMethod;
+
+/**
+ * Finds Enum.values() 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()
+ * </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, an instance of the Enum subclass class must be used.
+ * </ul>
+ */
+public class EnqueuerEnumReflectionAnalysisJava extends EnqueuerEnumReflectionAnalysisBase {
+  private final DexString enumSetAllOfString;
+  private final DexString enumSetNoneOfString;
+  private final DexString enumSetRangeString;
+  private final DexString enumSetOfString;
+  private final DexType javaUtilEnumMapType;
+  private final DexType javaUtilEnumSetType;
+  private final DexMethod enumValueOfMethod;
+  private final DexMethod enumMapConstructor;
+
+  public EnqueuerEnumReflectionAnalysisJava(
+      AppView<? extends AppInfoWithClassHierarchy> appView, Enqueuer enqueuer) {
+    super(appView, enqueuer);
+
+    DexItemFactory dexItemFactory = appView.dexItemFactory();
+    enumSetAllOfString = dexItemFactory.createString("allOf");
+    enumSetNoneOfString = dexItemFactory.createString("noneOf");
+    enumSetRangeString = dexItemFactory.createString("range");
+    enumSetOfString = dexItemFactory.ofMethodName;
+    javaUtilEnumMapType = dexItemFactory.createType("Ljava/util/EnumMap;");
+    javaUtilEnumSetType = dexItemFactory.createType("Ljava/util/EnumSet;");
+    enumValueOfMethod = dexItemFactory.enumMembers.valueOf;
+    enumMapConstructor =
+        dexItemFactory.createInstanceInitializer(javaUtilEnumMapType, dexItemFactory.classType);
+  }
+
+  private boolean isEnumSetFactoryMethod(DexMethod invokedMethod) {
+    if (!invokedMethod.getHolderType().isIdenticalTo(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)) {
+      enqueuer.addMethodThatRequireIrAnalysis(context);
+    }
+  }
+
+  @Override
+  public void traceInvokeDirect(
+      DexMethod invokedMethod, MethodResolutionResult resolutionResult, ProgramMethod context) {
+    // EnumMap uses reflection.
+    if (invokedMethod.isIdenticalTo(enumMapConstructor)) {
+      enqueuer.addMethodThatRequireIrAnalysis(context);
+    }
+  }
+
+  @Override
+  public boolean handleReflectiveInvoke(ProgramMethod method, InvokeMethod invoke) {
+    DexMethod invokedMethod = invoke.getInvokedMethod();
+    if (invokedMethod.isIdenticalTo(enumValueOfMethod)
+        || invokedMethod.isIdenticalTo(enumMapConstructor)
+        || isEnumSetFactoryMethod(invokedMethod)) {
+      handleReflectiveEnumInvoke(method, invoke, 0);
+      return true;
+    }
+    return false;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/EnqueuerMockitoAnalysis.java b/src/main/java/com/android/tools/r8/shaking/EnqueuerMockitoAnalysis.java
new file mode 100644
index 0000000..6b1ae52
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/EnqueuerMockitoAnalysis.java
@@ -0,0 +1,126 @@
+// 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.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.IrBasedEnqueuerAnalysis;
+import com.android.tools.r8.graph.analysis.TraceInvokeEnqueuerAnalysis;
+import com.android.tools.r8.ir.analysis.type.ArrayTypeElement;
+import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.Value;
+
+/** Ensure classes passed to Mockito.mock() and Mockito.spy() are not marked as "final". */
+class EnqueuerMockitoAnalysis implements TraceInvokeEnqueuerAnalysis, IrBasedEnqueuerAnalysis {
+
+  private final AppView<? extends AppInfoWithClassHierarchy> appView;
+  private final Enqueuer enqueuer;
+
+  public final DexType mockitoType;
+  public final DexString mockString;
+  public final DexString spyString;
+
+  public EnqueuerMockitoAnalysis(
+      AppView<? extends AppInfoWithClassHierarchy> appView, Enqueuer enqueuer) {
+    this.appView = appView;
+    this.enqueuer = enqueuer;
+
+    DexItemFactory dexItemFactory = appView.dexItemFactory();
+    mockitoType = dexItemFactory.createType("Lorg/mockito/Mockito;");
+    mockString = dexItemFactory.createString("mock");
+    spyString = dexItemFactory.createString("spy");
+  }
+
+  public static void register(
+      AppView<? extends AppInfoWithClassHierarchy> appView,
+      Enqueuer enqueuer,
+      EnqueuerAnalysisCollection.Builder builder) {
+    EnqueuerMockitoAnalysis instance = new EnqueuerMockitoAnalysis(appView, enqueuer);
+    builder.addTraceInvokeAnalysis(instance);
+    builder.addIrBasedEnqueuerAnalysis(instance);
+  }
+
+  private boolean isReflectiveMockInvoke(DexMethod invokedMethod) {
+    return invokedMethod.holder.isIdenticalTo(mockitoType)
+        && (invokedMethod.getName().isIdenticalTo(mockString)
+            || invokedMethod.getName().isIdenticalTo(spyString));
+  }
+
+  @Override
+  public void traceInvokeStatic(
+      DexMethod invokedMethod, MethodResolutionResult resolutionResult, ProgramMethod context) {
+    if (isReflectiveMockInvoke(invokedMethod)) {
+      enqueuer.addMethodThatRequireIrAnalysis(context);
+    }
+  }
+
+  @Override
+  public boolean handleReflectiveInvoke(ProgramMethod context, InvokeMethod invoke) {
+    DexMethod invokedMethod = invoke.getInvokedMethod();
+
+    if (!isReflectiveMockInvoke(invokedMethod)) {
+      return false;
+    }
+
+    DexItemFactory dexItemFactory = appView.dexItemFactory();
+    DexType mockedType;
+    if (invokedMethod.getParameter(0).isIdenticalTo(dexItemFactory.classType)) {
+      // Given an explicit const-cast
+      Value classValue = invoke.getFirstArgument();
+      if (!classValue.isConstClass()) {
+        return true;
+      }
+      mockedType = classValue.getDefinition().asConstClass().getType();
+    } else if (invokedMethod.getParameter(invokedMethod.getArity() - 1).isArrayType()) {
+      // This should always be an empty array of the mocked type.
+      Value arrayValue = invoke.getLastArgument();
+      ArrayTypeElement arrayType = arrayValue.getType().asArrayType();
+      if (arrayType == null) {
+        // Should never happen.
+        return true;
+      }
+      ClassTypeElement memberType = arrayType.getMemberType().asClassType();
+      if (memberType == null) {
+        return true;
+      }
+      mockedType = memberType.getClassType();
+    } else {
+      // Should be Mockito.spy(Object).
+      if (invokedMethod.getArity() != 1
+          || !invokedMethod.getParameter(0).isIdenticalTo(dexItemFactory.objectType)) {
+        return true;
+      }
+      Value objectValue = invoke.getFirstArgument();
+      if (objectValue == null || objectValue.isPhi()) {
+        return true;
+      }
+      ClassTypeElement classType = objectValue.getType().asClassType();
+      if (classType == null) {
+        return true;
+      }
+      mockedType = classType.toDexType(dexItemFactory);
+    }
+
+    DexClass dexClass = appView.definitionFor(mockedType, context);
+    if (dexClass == null || !dexClass.isProgramClass()) {
+      return true;
+    }
+
+    // Make sure the type is not made final so that it can still be subclassed by Mockito.
+    enqueuer
+        .getKeepInfo()
+        .joinClass(dexClass.asProgramClass(), joiner -> joiner.disallowOptimization());
+    return true;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/EnqueuerMockitoSupport.java b/src/main/java/com/android/tools/r8/shaking/EnqueuerMockitoSupport.java
deleted file mode 100644
index 19be910..0000000
--- a/src/main/java/com/android/tools/r8/shaking/EnqueuerMockitoSupport.java
+++ /dev/null
@@ -1,82 +0,0 @@
-// 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.DexClass;
-import com.android.tools.r8.graph.DexDefinitionSupplier;
-import com.android.tools.r8.graph.DexItemFactory;
-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.ir.analysis.type.ArrayTypeElement;
-import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
-import com.android.tools.r8.ir.code.InvokeMethod;
-import com.android.tools.r8.ir.code.Value;
-import com.android.tools.r8.shaking.KeepInfoCollection.MutableKeepInfoCollection;
-
-class EnqueuerMockitoSupport {
-
-  static boolean isReflectiveMockInvoke(DexItemFactory dexItemFactory, DexMethod invokedMethod) {
-    return invokedMethod.holder.isIdenticalTo(dexItemFactory.mockitoType)
-        && (invokedMethod.getName().isIdenticalTo(dexItemFactory.mockString)
-            || invokedMethod.getName().isIdenticalTo(dexItemFactory.spyString));
-  }
-
-  /** Ensure classes passed to Mockito.mock() and Mockito.spy() are not marked as "final". */
-  static void handleReflectiveMockInvoke(
-      DexDefinitionSupplier appView,
-      MutableKeepInfoCollection keepInfo,
-      ProgramMethod context,
-      InvokeMethod invoke) {
-    DexMethod method = invoke.getInvokedMethod();
-    DexItemFactory dexItemFactory = appView.dexItemFactory();
-
-    DexType mockedType;
-    if (method.getParameter(0).isIdenticalTo(dexItemFactory.classType)) {
-      // Given an explicit const-cast
-      Value classValue = invoke.getFirstArgument();
-      if (!classValue.isConstClass()) {
-        return;
-      }
-      mockedType = classValue.getDefinition().asConstClass().getType();
-    } else if (method.getParameter(method.getArity() - 1).isArrayType()) {
-      // This should always be an empty array of the mocked type.
-      Value arrayValue = invoke.getLastArgument();
-      ArrayTypeElement arrayType = arrayValue.getType().asArrayType();
-      if (arrayType == null) {
-        // Should never happen.
-        return;
-      }
-      ClassTypeElement memberType = arrayType.getMemberType().asClassType();
-      if (memberType == null) {
-        return;
-      }
-      mockedType = memberType.getClassType();
-    } else {
-      // Should be Mockito.spy(Object).
-      if (method.getArity() != 1
-          || !method.getParameter(0).isIdenticalTo(dexItemFactory.objectType)) {
-        return;
-      }
-      Value objectValue = invoke.getFirstArgument();
-      if (objectValue == null || objectValue.isPhi()) {
-        return;
-      }
-      ClassTypeElement classType = objectValue.getType().asClassType();
-      if (classType == null) {
-        return;
-      }
-      mockedType = classType.toDexType(dexItemFactory);
-    }
-
-    DexClass dexClass = appView.definitionFor(mockedType, context);
-    if (dexClass == null || !dexClass.isProgramClass()) {
-      return;
-    }
-
-    // Make sure the type is not made final so that it can still be subclassed by Mockito.
-    keepInfo.joinClass(dexClass.asProgramClass(), joiner -> joiner.disallowOptimization());
-  }
-}
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 64de864..b408a09 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 experimentalTraceAndroidEnumSerialization =
+      System.getProperty("com.android.tools.r8.experimentalTraceAndroidEnumSerialization") != 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/shaking/enums/EnumReflectionAndroidTest.java b/src/test/java/com/android/tools/r8/shaking/enums/EnumReflectionAndroidTest.java
new file mode 100644
index 0000000..83921be
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/enums/EnumReflectionAndroidTest.java
@@ -0,0 +1,323 @@
+// 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.EnumReflectionAndroidTest.Helpers.EnumA;
+import com.android.tools.r8.shaking.enums.EnumReflectionAndroidTest.Helpers.EnumB;
+import com.android.tools.r8.shaking.enums.EnumReflectionAndroidTest.Helpers.EnumC;
+import com.android.tools.r8.shaking.enums.EnumReflectionAndroidTest.Helpers.EnumD;
+import com.android.tools.r8.shaking.enums.EnumReflectionAndroidTest.Helpers.EnumE;
+import com.android.tools.r8.shaking.enums.EnumReflectionAndroidTest.Helpers.EnumF;
+import com.android.tools.r8.shaking.enums.EnumReflectionAndroidTest.Helpers.EnumG;
+import com.android.tools.r8.shaking.enums.EnumReflectionAndroidTest.Helpers.EnumH;
+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.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 Android-specific APIs that cause an Enum type's values() method to be kept. */
+@RunWith(Parameterized.class)
+public class EnumReflectionAndroidTest 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(
+          "bundle: A",
+          "bundle: B",
+          "parcel: C",
+          "parcel: D",
+          "intent: E",
+          "intent: F",
+          "array: [G]",
+          "array: [H]");
+
+  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,
+    }
+
+    public enum EnumF {
+      F,
+      G,
+    }
+
+    public enum EnumG {
+      G {},
+      H,
+    }
+
+    public enum EnumH {
+      H,
+      I,
+    }
+  }
+
+  public static class TestMain {
+    @NeverInline
+    private static void androidBundle1() {
+      FakeBundle b = FakeBundle.createWithSingleSerializable(EnumA.A);
+      EnumA result = (EnumA) b.getSerializable("");
+      System.out.println("bundle: " + result);
+    }
+
+    @NeverInline
+    private static void androidBundle2() {
+      FakeBundle b = FakeBundle.createWithSingleSerializable(EnumB.B);
+      EnumB result = b.getSerializable("", EnumB.class);
+      System.out.println("bundle: " + result);
+    }
+
+    @NeverInline
+    private static void androidParcel1() {
+      FakeParcel p = FakeParcel.createWithSingleSerializable(EnumC.C);
+      EnumC result = (EnumC) p.readSerializable();
+      System.out.println("parcel: " + result);
+    }
+
+    @NeverInline
+    private static void androidParcel2() {
+      FakeParcel p = FakeParcel.createWithSingleSerializable(EnumD.D);
+      System.out.println("parcel: " + p.readSerializable(null, EnumD.class));
+    }
+
+    @NeverInline
+    private static void androidIntent1() {
+      FakeIntent i = FakeIntent.createWithSingleSerializable(EnumE.E);
+      EnumE result = (EnumE) i.getSerializableExtra("");
+      System.out.println("intent: " + result);
+    }
+
+    @NeverInline
+    private static void androidIntent2() {
+      FakeIntent i = FakeIntent.createWithSingleSerializable(EnumF.F);
+      System.out.println("intent: " + i.getSerializableExtra("", EnumF.class));
+    }
+
+    @NeverInline
+    private static void array1() {
+      FakeParcel p = FakeParcel.createWithSingleSerializable(new EnumG[] {EnumG.G});
+      EnumG[] result = (EnumG[]) p.readSerializable();
+      System.out.println("array: " + Arrays.toString(result));
+    }
+
+    @NeverInline
+    private static void array2() {
+      FakeParcel p = FakeParcel.createWithSingleSerializable(new EnumH[] {EnumH.H});
+      System.out.println("array: " + Arrays.toString(p.readSerializable(null, EnumH[].class)));
+    }
+
+    public static void main(String[] args) {
+      // Use different methods to ensure Enqueuer.traceInvokeStatic() triggers for each one.
+      androidBundle1();
+      androidBundle2();
+      androidParcel1();
+      androidParcel2();
+      androidIntent1();
+      androidIntent2();
+      array1();
+      array2();
+    }
+  }
+
+  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;";
+  // EnumE.E and EnumG.G references this class in its constructor.
+  private static final String ENUM_SUBTYPE_BRIDGE_CLASS_NAME =
+      EnumReflectionAndroidTest.class.getName() + "$1";
+
+  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.experimentalTraceAndroidEnumSerialization = 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);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/enums/EnumCollectionsTest.java b/src/test/java/com/android/tools/r8/shaking/enums/EnumReflectionJavaTest.java
similarity index 72%
rename from src/test/java/com/android/tools/r8/shaking/enums/EnumCollectionsTest.java
rename to src/test/java/com/android/tools/r8/shaking/enums/EnumReflectionJavaTest.java
index 2a9872c..9c8f0cd 100644
--- a/src/test/java/com/android/tools/r8/shaking/enums/EnumCollectionsTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/enums/EnumReflectionJavaTest.java
@@ -1,4 +1,4 @@
-// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// 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.
 
@@ -8,6 +8,17 @@
 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.EnumReflectionJavaTest.Helpers.EnumA;
+import com.android.tools.r8.shaking.enums.EnumReflectionJavaTest.Helpers.EnumB;
+import com.android.tools.r8.shaking.enums.EnumReflectionJavaTest.Helpers.EnumC;
+import com.android.tools.r8.shaking.enums.EnumReflectionJavaTest.Helpers.EnumD;
+import com.android.tools.r8.shaking.enums.EnumReflectionJavaTest.Helpers.EnumE;
+import com.android.tools.r8.shaking.enums.EnumReflectionJavaTest.Helpers.EnumF;
+import com.android.tools.r8.shaking.enums.EnumReflectionJavaTest.Helpers.EnumG;
+import com.android.tools.r8.shaking.enums.EnumReflectionJavaTest.Helpers.EnumH;
+import com.android.tools.r8.shaking.enums.EnumReflectionJavaTest.Helpers.EnumI;
+import com.android.tools.r8.shaking.enums.EnumReflectionJavaTest.Helpers.EnumJ;
+import com.android.tools.r8.shaking.enums.EnumReflectionJavaTest.Helpers.EnumK;
 import java.util.Arrays;
 import java.util.EnumMap;
 import java.util.EnumSet;
@@ -18,8 +29,9 @@
 import org.junit.runners.Parameterized.Parameter;
 import org.junit.runners.Parameterized.Parameters;
 
+/** Tests for the non-Android-specific APIs that cause an Enum type's values() method to be kept. */
 @RunWith(Parameterized.class)
-public class EnumCollectionsTest extends TestBase {
+public class EnumReflectionJavaTest extends TestBase {
 
   @Parameter(0)
   public TestParameters parameters;
@@ -44,7 +56,8 @@
           "valueOf: K",
           "phi: [B]");
 
-  public static class TestMain {
+  public static class Helpers {
+
     public enum EnumA {
       A,
       B
@@ -56,7 +69,7 @@
     }
 
     public enum EnumC {
-      C,
+      C {}, // Test anonymous subtype.
       D
     }
 
@@ -109,7 +122,9 @@
       K,
       L
     }
+  }
 
+  public static class TestMain {
     @NeverInline
     private static void noneOf() {
       System.out.println("none: " + EnumSet.complementOf(EnumSet.noneOf(EnumA.class)));
@@ -186,10 +201,16 @@
     }
   }
 
+  // EnumC.C references this class in its constructor.
+  private static final String ENUM_SUBTYPE_BRIDGE_CLASS_NAME =
+      EnumReflectionJavaTest.class.getName() + "$1";
+
   @Test
   public void testRuntime() throws Exception {
     testForRuntime(parameters)
-        .addProgramClassesAndInnerClasses(TestMain.class)
+        .addProgramClassesAndInnerClasses(Helpers.class)
+        .addProgramClasses(TestMain.class)
+        .addProgramClasses(Class.forName(ENUM_SUBTYPE_BRIDGE_CLASS_NAME))
         .run(parameters.getRuntime(), TestMain.class)
         .assertSuccessWithOutputLines(EXPECTED_OUTPUT);
   }
@@ -198,7 +219,9 @@
   public void testR8() throws Exception {
     testForR8(parameters.getBackend())
         .setMinApi(parameters)
-        .addProgramClassesAndInnerClasses(TestMain.class)
+        .addProgramClassesAndInnerClasses(Helpers.class)
+        .addProgramClasses(TestMain.class)
+        .addProgramClasses(Class.forName(ENUM_SUBTYPE_BRIDGE_CLASS_NAME))
         .enableInliningAnnotations()
         .addKeepMainRule(TestMain.class)
         .compile()