Refactor optimized resource shrinker to be an Enqueuer Extension

Simplify the interaction between Enqueuer and R8ResourceShrinkerState by
introducing ResourceShrinkerEnqueuerExtension.

Change-Id: Ic51492fd3ec3153245d52e6d135f46d1e51a4a4f
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 b7bbe6f..1b8428c 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
@@ -58,7 +58,8 @@
   private final NewlyTargetedMethodEnqueuerAnalysis[] newlyTargetedMethodAnalyses;
   private final MarkFieldAsKeptEnqueuerAnalysis[] markFieldAsKeptEnqueuerAnalyses;
 
-  // Tear down events.
+  // Start & Tear down events.
+  private final StartEnqueuerAnalysis[] startAnalyses;
   private final FinishedEnqueuerAnalysis[] finishedAnalyses;
   private final FixpointEnqueuerAnalysis[] fixpointAnalyses;
 
@@ -86,9 +87,11 @@
       NewlyReferencedFieldEnqueuerAnalysis[] newlyReferencedFieldAnalyses,
       NewlyTargetedMethodEnqueuerAnalysis[] newlyTargetedMethodAnalyses,
       MarkFieldAsKeptEnqueuerAnalysis[] markFieldAsKeptEnqueuerAnalyses,
-      // Tear down events.
+      // Start & Tear down events.
+      StartEnqueuerAnalysis[] startAnalyses,
       FinishedEnqueuerAnalysis[] finishedAnalyses,
       FixpointEnqueuerAnalysis[] fixpointAnalyses) {
+    this.startAnalyses = startAnalyses;
     // Trace events.
     this.checkCastAnalyses = checkCastAnalyses;
     this.constClassAnalyses = constClassAnalyses;
@@ -367,6 +370,12 @@
     }
   }
 
+  public void onStarted(Enqueuer enqueuer) {
+    for (StartEnqueuerAnalysis analysis : startAnalyses) {
+      analysis.onStarted(enqueuer);
+    }
+  }
+
   // Tear down events.
 
   public void done(Enqueuer enqueuer, ExecutorService executorService) throws ExecutionException {
@@ -431,6 +440,9 @@
         new ArrayList<>();
     private final List<MarkFieldAsKeptEnqueuerAnalysis> markFieldAsKeptAnalyses = new ArrayList<>();
 
+    // Start events.
+    private final List<StartEnqueuerAnalysis> startAnalyses = new ArrayList<>();
+
     // Tear down events.
     private final List<FinishedEnqueuerAnalysis> finishedAnalyses = new ArrayList<>();
     private final List<FixpointEnqueuerAnalysis> fixpointAnalyses = new ArrayList<>();
@@ -546,6 +558,11 @@
       return this;
     }
 
+    public Builder addStartAnalysis(StartEnqueuerAnalysis analysis) {
+      startAnalyses.add(analysis);
+      return this;
+    }
+
     // Tear down events.
 
     public Builder addFinishedAnalysis(FinishedEnqueuerAnalysis analysis) {
@@ -585,6 +602,7 @@
           newlyTargetedMethodAnalyses.toArray(NewlyTargetedMethodEnqueuerAnalysis[]::new),
           markFieldAsKeptAnalyses.toArray(MarkFieldAsKeptEnqueuerAnalysis[]::new),
           // Tear down events.
+          startAnalyses.toArray(StartEnqueuerAnalysis[]::new),
           finishedAnalyses.toArray(FinishedEnqueuerAnalysis[]::new),
           fixpointAnalyses.toArray(FixpointEnqueuerAnalysis[]::new));
     }
diff --git a/src/main/java/com/android/tools/r8/graph/analysis/StartEnqueuerAnalysis.java b/src/main/java/com/android/tools/r8/graph/analysis/StartEnqueuerAnalysis.java
new file mode 100644
index 0000000..8284fc4
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/analysis/StartEnqueuerAnalysis.java
@@ -0,0 +1,12 @@
+// Copyright (c) 2026, 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.shaking.Enqueuer;
+
+public interface StartEnqueuerAnalysis {
+
+  /** Called when the Enqueuer starts tracing. */
+  void onStarted(Enqueuer enqueuer);
+}
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 61ed436..c4867fc 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -17,7 +17,6 @@
 import static java.util.Collections.emptySet;
 
 import com.android.tools.r8.Diagnostic;
-import com.android.tools.r8.FeatureSplit;
 import com.android.tools.r8.cf.code.CfInstruction;
 import com.android.tools.r8.cf.code.CfInvoke;
 import com.android.tools.r8.contexts.CompilationContext.MethodProcessingContext;
@@ -124,9 +123,7 @@
 import com.android.tools.r8.naming.IdentifierNameStringCollection;
 import com.android.tools.r8.optimize.interfaces.analysis.CfOpenClosedInterfacesAnalysis;
 import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.origin.PathOrigin;
 import com.android.tools.r8.profile.rewriting.ProfileCollectionAdditions;
-import com.android.tools.r8.resourceshrinker.ResourceShrinkerState;
 import com.android.tools.r8.shaking.AnnotationMatchResult.MatchedAnnotation;
 import com.android.tools.r8.shaking.EnqueuerEvent.ClassEnqueuerEvent;
 import com.android.tools.r8.shaking.EnqueuerEvent.InstantiatedClassEnqueuerEvent;
@@ -175,7 +172,6 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
-import java.nio.file.Paths;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -304,6 +300,8 @@
    */
   private final Map<DexProgramClass, ProgramMethod> synthesizingContexts = new IdentityHashMap<>();
 
+  private ResourceShrinkerEnqueuerExtension resourceShrinkerExtension = null;
+
   /**
    * Set of types that are mentioned in the program. We at least need an empty abstract class item
    * for these.
@@ -530,13 +528,6 @@
     this.initialPrunedTypes = initialPrunedTypes;
     this.prunedClasspathTypes = prunedClasspathTypes;
 
-    if (options.isOptimizedResourceShrinking()) {
-      ResourceShrinkerState<FeatureSplit> resourceShrinkerState =
-          appView.getResourceShrinkerState();
-      resourceShrinkerState.setEnqueuerCallback(this::recordReferenceFromResources);
-      resourceShrinkerState.setEnqueuerMethodCallback(this::recordMethodReferenceFromResources);
-    }
-
     EnqueuerAnalysisCollection.Builder analysesBuilder = EnqueuerAnalysisCollection.builder();
     if (mode.isTreeShaking()) {
       EnqueuerDefinitionSupplier enqueuerDefinitionSupplier = new EnqueuerDefinitionSupplier(this);
@@ -554,7 +545,8 @@
       KotlinMetadataEnqueuerExtension.register(
           appView, enqueuerDefinitionSupplier, initialPrunedTypes, analysesBuilder);
       ProtoEnqueuerExtension.register(appView, this, analysesBuilder);
-      ResourceShrinkerEnqueuerExtension.register(appView, this, analysesBuilder);
+      this.resourceShrinkerExtension =
+          ResourceShrinkerEnqueuerExtension.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
@@ -697,15 +689,7 @@
     recordTypeReference(type, context, this::recordNonProgramClass, this::reportMissingClass);
   }
 
-  private final Map<DexString, Origin> onClickMethodReferences = new HashMap<>();
-
-  private void recordMethodReferenceFromResources(String method, String xmlFilePath) {
-    Origin origin = new PathOrigin(Paths.get(xmlFilePath));
-    onClickMethodReferences.put(appView.dexItemFactory().createString(method), origin);
-  }
-
-  private boolean recordReferenceFromResources(
-      String possibleClass, String xmlFilePath, boolean markAsLive) {
+  boolean recordReferenceFromResources(String possibleClass, Origin origin, boolean markAsLive) {
     if (!DescriptorUtils.isValidJavaType(possibleClass)) {
       return false;
     }
@@ -722,7 +706,6 @@
       return false;
     }
 
-    Origin origin = new PathOrigin(Paths.get(xmlFilePath));
     ReflectiveUseFromXml reason = KeepReason.reflectiveUseFromXml(origin);
     ensureClassKeptForResourceLookup(clazz, reason, markAsLive);
 
@@ -1269,7 +1252,9 @@
     if (mode.isInitialTreeShaking()) {
       return;
     }
-    appView.getResourceShrinkerState().trace(value, "from dex");
+    if (resourceShrinkerExtension != null) {
+      resourceShrinkerExtension.traceResourceValue(value);
+    }
   }
 
   public void traceReflectiveFieldWrite(ProgramField field, ProgramMethod context) {
@@ -2410,39 +2395,6 @@
     analyses.processNewlyLiveClass(clazz, worklist);
   }
 
-  private void processOnClickMethods(Timing timing) {
-    if (onClickMethodReferences.isEmpty()) {
-      return;
-    }
-    timing.begin("Process onclick methods");
-    for (DexProgramClass item : liveTypes.getItems()) {
-      for (ProgramMethod method :
-          item.virtualProgramMethods(
-              p ->
-                  p.getParameters().size() == 1
-                      && p.getParameter(0)
-                          .isIdenticalTo(appInfo.dexItemFactory().androidViewViewType)
-                      && onClickMethodReferences.containsKey(p.getName()))) {
-        KeepMethodInfo methodInfo = getKeepInfo().getMethodInfo(method);
-        if (!methodInfo.isOptimizationAllowed(options)
-            && !methodInfo.isShrinkingAllowed(options)
-            && !methodInfo.isMinificationAllowed(options)) {
-          continue;
-        }
-        ReflectiveUseFromXml reason =
-            KeepReason.reflectiveUseFromXml(onClickMethodReferences.get(method.getName()));
-        Joiner minimumKeepInfo =
-            KeepMethodInfo.newEmptyJoiner()
-                .disallowOptimization()
-                .disallowShrinking()
-                .disallowMinification()
-                .addReason(reason);
-        applyMinimumKeepInfo(method, minimumKeepInfo);
-      }
-    }
-    timing.end();
-  }
-
   private void ensureMethodsContinueToWidenAccess(ClassDefinition clazz) {
     assert !clazz.isProgramClass();
     ScopedDexMethodSet seen =
@@ -3958,6 +3910,7 @@
     assert analyses.isEmpty();
     assert mode.isMainDexTracing();
     this.rootSet = appView.getMainDexRootSet();
+    analyses.onStarted(this);
     // Translate the result of root-set computation into enqueuer actions.
     includeMinimumKeepInfo(rootSet);
     trace(executorService, timing);
@@ -3979,6 +3932,7 @@
   public EnqueuerResult traceApplication(
       RootSet rootSet, ExecutorService executorService, Timing timing) throws ExecutionException {
     this.rootSet = rootSet;
+    analyses.onStarted(this);
     rootSet.pendingMethodMoveInverse.forEach(pendingMethodMoveInverse::put);
 
     // Transfer the minimum keep info from the root set into the Enqueuer state.
@@ -4008,7 +3962,6 @@
     enqueueAllIfNotShrinking();
     timing.end();
     timing.begin("Trace");
-    traceManifestsAndRoots(timing);
     trace(executorService, timing);
     timing.end();
     options.reporter.failIfPendingErrors();
@@ -4018,9 +3971,6 @@
     timing.begin("Finish analysis");
     taskCollection.awaitEnqueuerIndependentTasks();
     analyses.done(this, executorService);
-    if (appView.options().isOptimizedResourceShrinking()) {
-      appView.getResourceShrinkerState().enqueuerDone(this.mode.isFinalTreeShaking());
-    }
     timing.end();
     assert verifyKeptGraph();
     timing.begin("Finish compat building");
@@ -4043,17 +3993,6 @@
     return result;
   }
 
-  private void traceManifestsAndRoots(Timing timing) {
-    if (options.isOptimizedResourceShrinking()) {
-      timing.begin("Trace AndroidManifest.xml files");
-      appView.getResourceShrinkerState().traceKeepXmlAndManifest();
-      for (int rootResourceId : appView.rootSet().resourceIds) {
-        appView.getResourceShrinkerState().trace(rootResourceId, "Non shrunken dex code");
-      }
-      timing.end();
-    }
-  }
-
   private void includeMinimumKeepInfo(RootSetBase rootSet) {
     rootSet
         .getDependentMinimumKeepInfo()
@@ -5027,12 +4966,6 @@
           }
         }
 
-        processOnClickMethods(timing);
-        if (worklist.hasNext()) {
-          timing.end();
-          continue;
-        }
-
         // Continue fix-point processing while there are additional work items to ensure items that
         // are passed to Java reflections are traced.
         reflectiveIdentification.processWorklist(timing);
diff --git a/src/main/java/com/android/tools/r8/shaking/ResourceShrinkerEnqueuerExtension.java b/src/main/java/com/android/tools/r8/shaking/ResourceShrinkerEnqueuerExtension.java
index 4642cb3..bc50d85 100644
--- a/src/main/java/com/android/tools/r8/shaking/ResourceShrinkerEnqueuerExtension.java
+++ b/src/main/java/com/android/tools/r8/shaking/ResourceShrinkerEnqueuerExtension.java
@@ -11,16 +11,20 @@
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexField;
 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.DexValue;
-import com.android.tools.r8.graph.DexValue.DexValueResourceNumber;
 import com.android.tools.r8.graph.FieldResolutionResult.SingleFieldResolutionResult;
 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.graph.analysis.EnqueuerAnalysisCollection;
+import com.android.tools.r8.graph.analysis.FinishedEnqueuerAnalysis;
+import com.android.tools.r8.graph.analysis.FixpointEnqueuerAnalysis;
 import com.android.tools.r8.graph.analysis.MarkFieldAsKeptEnqueuerAnalysis;
+import com.android.tools.r8.graph.analysis.NewlyLiveClassEnqueuerAnalysis;
 import com.android.tools.r8.graph.analysis.NewlyLiveFieldEnqueuerAnalysis;
+import com.android.tools.r8.graph.analysis.StartEnqueuerAnalysis;
 import com.android.tools.r8.graph.analysis.TraceFieldAccessEnqueuerAnalysis;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
@@ -28,74 +32,104 @@
 import com.android.tools.r8.ir.code.StaticPut;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.conversion.MethodConversionOptions;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.origin.PathOrigin;
 import com.android.tools.r8.resourceshrinker.ResourceShrinkerState;
+import com.android.tools.r8.resourceshrinker.ResourceShrinkerState.ResourceShrinkerCallback;
+import com.android.tools.r8.utils.collections.ProgramMethodSet;
 import com.android.tools.r8.utils.internal.exceptions.Unreachable;
+import com.google.common.collect.Sets;
 import it.unimi.dsi.fastutil.ints.IntArrayList;
 import it.unimi.dsi.fastutil.ints.IntList;
+import java.nio.file.Paths;
 import java.util.IdentityHashMap;
 import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
 
 public class ResourceShrinkerEnqueuerExtension
-    implements TraceFieldAccessEnqueuerAnalysis, MarkFieldAsKeptEnqueuerAnalysis {
+    implements TraceFieldAccessEnqueuerAnalysis,
+        MarkFieldAsKeptEnqueuerAnalysis,
+        NewlyLiveClassEnqueuerAnalysis,
+        NewlyLiveFieldEnqueuerAnalysis,
+        StartEnqueuerAnalysis,
+        FixpointEnqueuerAnalysis,
+        FinishedEnqueuerAnalysis,
+        ResourceShrinkerCallback {
 
-  private final ResourceShrinkerState<FeatureSplit> resourceShrinkerState;
-  private final Map<DexType, RClassFieldToValueStore> fieldToValueMapping = new IdentityHashMap<>();
   private final AppView<? extends AppInfoWithClassHierarchy> appView;
   private final Enqueuer enqueuer;
+  private final ResourceShrinkerState<FeatureSplit> resourceShrinkerState;
+  private final Map<DexType, RClassFieldToValueStore> fieldToValueMapping = new IdentityHashMap<>();
+
+  // Deferred state
+  private final Map<DexString, Origin> onClickMethodReferences = new IdentityHashMap<>();
+  private final ProgramMethodSet processedOnClickMethods = ProgramMethodSet.create();
+
+  // Pending incremental state
+  private final Set<DexProgramClass> pendingLiveClasses = Sets.newIdentityHashSet();
+  private final Map<DexString, Origin> pendingOnClickMethodReferences = new IdentityHashMap<>();
 
   private ResourceShrinkerEnqueuerExtension(
       AppView<? extends AppInfoWithClassHierarchy> appView, Enqueuer enqueuer) {
     this.appView = appView;
-    this.resourceShrinkerState = appView.getResourceShrinkerState();
     this.enqueuer = enqueuer;
+    this.resourceShrinkerState = appView.getResourceShrinkerState();
   }
 
-  public static void register(
+  public static ResourceShrinkerEnqueuerExtension register(
       AppView<? extends AppInfoWithClassHierarchy> appView,
       Enqueuer enqueuer,
       EnqueuerAnalysisCollection.Builder builder) {
-    if (fieldAccessAnalysisEnabled(appView, enqueuer)) {
-      ResourceShrinkerEnqueuerExtension analysis =
+    if (appView.options().isOptimizedResourceShrinking()) {
+      ResourceShrinkerEnqueuerExtension extension =
           new ResourceShrinkerEnqueuerExtension(appView, enqueuer);
-      builder.addTraceFieldAccessAnalysis(analysis);
-      builder.addMarkFieldAsKeptAnalysis(analysis);
+
+      // Always register for started, fixpoint, finished, and newly live class events
+      builder.addStartAnalysis(extension);
+      builder.addFixpointAnalysis(extension);
+      builder.addFinishedAnalysis(extension);
+      builder.addNewlyLiveClassAnalysis(extension);
+
+      if (fieldAccessAnalysisEnabled(enqueuer)) {
+        builder.addTraceFieldAccessAnalysis(extension);
+        builder.addMarkFieldAsKeptAnalysis(extension);
+      }
+      if (liveFieldAnalysisEnabled(appView, enqueuer)) {
+        builder.addNewlyLiveFieldAnalysis(extension);
+      }
+      return extension;
     }
-    if (liveFieldAnalysisEnabled(appView, enqueuer)) {
-      builder.addNewlyLiveFieldAnalysis(
-          new NewlyLiveFieldEnqueuerAnalysis() {
-            @Override
-            public void processNewlyLiveField(
-                ProgramField field, ProgramDefinition context, EnqueuerWorklist worklist) {
-              DexEncodedField definition = field.getDefinition();
-              if (field.getAccessFlags().isStatic()
-                  && definition.hasExplicitStaticValue()
-                  && definition.getStaticValue().isDexValueResourceNumber()) {
-                appView
-                    .getResourceShrinkerState()
-                    .trace(
-                        definition.getStaticValue().asDexValueResourceNumber().getValue(),
-                        // TODO(b/378625969): Consider wrapping this in a reachability structure
-                        // to avoid decoding.
-                        field.toString());
-              }
-            }
-          });
-    }
+    return null;
   }
 
   private static boolean liveFieldAnalysisEnabled(
       AppView<? extends AppInfoWithClassHierarchy> appView, Enqueuer enqueuer) {
     return appView.options().androidResourceProvider != null
-        && appView.options().isOptimizedResourceShrinking()
         && enqueuer.getMode().isFinalTreeShaking();
   }
 
-  private static boolean fieldAccessAnalysisEnabled(
-      AppView<? extends AppInfoWithClassHierarchy> appView, Enqueuer enqueuer) {
-    return appView.options().isOptimizedResourceShrinking()
-        // Only run this in the first round, we explicitly trace the resource values
-        // with ResourceConstNumber in the optimizing pipeline.
-        && enqueuer.getMode().isInitialTreeShaking();
+  private static boolean fieldAccessAnalysisEnabled(Enqueuer enqueuer) {
+    return enqueuer.getMode().isInitialTreeShaking();
+  }
+
+  // ClassReferenceCallback (called from ResourceShrinkerState during trace)
+  @Override
+  public boolean tryClass(String possibleClass, String xmlFilePath, boolean markAsLive) {
+    Origin xmlFileOrigin = new PathOrigin(Paths.get(xmlFilePath));
+    return enqueuer.recordReferenceFromResources(possibleClass, xmlFileOrigin, markAsLive);
+  }
+
+  @Override
+  public void tryMethod(String methodName, String xmlFilePath) {
+    Origin xmlFileOrigin = new PathOrigin(Paths.get(xmlFilePath));
+    pendingOnClickMethodReferences.put(
+        appView.dexItemFactory().createString(methodName), xmlFileOrigin);
+  }
+
+  public void traceResourceValue(int value) {
+    resourceShrinkerState.trace(value, "from dex", this);
   }
 
   @Override
@@ -122,57 +156,137 @@
       if (!fieldToValueMapping.containsKey(holderType)) {
         populateRClassValues(resolvedField.getHolder());
       }
-      assert fieldToValueMapping.containsKey(holderType);
       RClassFieldToValueStore rClassFieldToValueStore = fieldToValueMapping.get(holderType);
       IntList integers = rClassFieldToValueStore.valueMapping.get(resolvedField.getReference());
       // The R class can have fields injected, e.g., by jacoco, we don't have resource values for
       // these.
       if (integers != null) {
-        for (Integer integer : integers) {
-          resourceShrinkerState.trace(integer, resolvedField.getReference().toString());
+        for (int id : integers) {
+          resourceShrinkerState.trace(id, resolvedField.getReference().toString(), this);
         }
       }
     }
   }
 
+  @Override
+  public void processNewlyLiveClass(DexProgramClass clazz, EnqueuerWorklist worklist) {
+    pendingLiveClasses.add(clazz);
+    // Warn on final ID fields if needed
+    if (enqueuer.isRClass(clazz)) {
+      for (DexEncodedField field : clazz.staticFields()) {
+        if (field.isFinal() && field.hasExplicitStaticValue() && field.getType().isIntType()) {
+          appView
+              .reporter()
+              .warning(
+                  new FinalRClassEntriesWithOptimizedShrinkingDiagnostic(
+                      clazz.origin, field.getReference()));
+        }
+      }
+    }
+  }
+
+  @Override
+  public void processNewlyLiveField(
+      ProgramField field, ProgramDefinition context, EnqueuerWorklist worklist) {
+    DexEncodedField definition = field.getDefinition();
+    if (field.getAccessFlags().isStatic()
+        && definition.hasExplicitStaticValue()
+        && definition.getStaticValue().isDexValueResourceNumber()) {
+      resourceShrinkerState.trace(
+          definition.getStaticValue().asDexValueResourceNumber().getValue(),
+          field.getReference().toString(),
+          this);
+    }
+  }
+
+  @Override
+  public void onStarted(Enqueuer enqueuer) {
+    if (!enqueuer.getMode().isTreeShaking()) {
+      return;
+    }
+    resourceShrinkerState.traceKeepXmlAndManifest(this);
+    for (int rootResourceId : appView.rootSet().resourceIds) {
+      resourceShrinkerState.trace(rootResourceId, "Non shrunken dex code", this);
+    }
+  }
+
+  @Override
+  public void notifyFixpoint(
+      Enqueuer enqueuer,
+      EnqueuerWorklist worklist,
+      ExecutorService executorService,
+      com.android.tools.r8.utils.timing.Timing timing)
+      throws ExecutionException {
+    if (pendingLiveClasses.isEmpty() && pendingOnClickMethodReferences.isEmpty()) {
+      return;
+    }
+
+    // 1. Match pendingLiveClasses against committed onClickMethodReferences
+    if (!onClickMethodReferences.isEmpty()) {
+      for (DexProgramClass clazz : pendingLiveClasses) {
+        matchOnClickMethods(clazz, onClickMethodReferences);
+      }
+    }
+
+    // 2. Match pendingOnClickMethodReferences against ALL liveClasses
+    if (!pendingOnClickMethodReferences.isEmpty()) {
+      enqueuer.forAllLiveClasses(
+          clazz -> matchOnClickMethods(clazz, pendingOnClickMethodReferences));
+    }
+
+    // 3. Commit pending states
+    pendingLiveClasses.clear();
+    onClickMethodReferences.putAll(pendingOnClickMethodReferences);
+    pendingOnClickMethodReferences.clear();
+  }
+
+  private void matchOnClickMethods(
+      DexProgramClass clazz, Map<DexString, Origin> onClickReferences) {
+    for (ProgramMethod method :
+        clazz.virtualProgramMethods(
+            p ->
+                p.getParameters().size() == 1
+                    && p.getParameter(0).isIdenticalTo(appView.dexItemFactory().androidViewViewType)
+                    && onClickReferences.containsKey(p.getName()))) {
+      if (processedOnClickMethods.add(method)) {
+        KeepMethodInfo methodInfo = enqueuer.getKeepInfo().getMethodInfo(method);
+        if (!methodInfo.isOptimizationAllowed(appView.options())
+            && !methodInfo.isShrinkingAllowed(appView.options())
+            && !methodInfo.isMinificationAllowed(appView.options())) {
+          continue;
+        }
+        KeepReason reason =
+            KeepReason.reflectiveUseFromXml(onClickReferences.get(method.getName()));
+        KeepMethodInfo.Joiner minimumKeepInfo =
+            KeepMethodInfo.newEmptyJoiner()
+                .disallowOptimization()
+                .disallowShrinking()
+                .disallowMinification()
+                .addReason(reason);
+        enqueuer.applyMinimumKeepInfo(method, minimumKeepInfo);
+      }
+    }
+  }
+
+  @Override
+  public void done(Enqueuer enqueuer, ExecutorService executorService) {
+    resourceShrinkerState.enqueuerDone(enqueuer.getMode().isFinalTreeShaking());
+  }
+
   private void populateRClassValues(DexProgramClass programClass) {
-    // TODO(287398085): Pending discussions with the AAPT2 team, we might need to harden this
-    // to not fail if we wrongly classify an unrelated class as R class in our heuristic..
     RClassFieldToValueStore.Builder rClassValueBuilder = new RClassFieldToValueStore.Builder();
     analyzeStaticFields(programClass, rClassValueBuilder);
     ProgramMethod programClassInitializer = programClass.getProgramClassInitializer();
     if (programClassInitializer != null) {
       analyzeClassInitializer(rClassValueBuilder, programClassInitializer);
     }
-    warnOnFinalIdFields(programClass);
     fieldToValueMapping.put(programClass.getType(), rClassValueBuilder.build());
   }
 
-  private void warnOnFinalIdFields(DexProgramClass holder) {
-    if (!appView.options().isOptimizedResourceShrinking()) {
-      return;
-    }
-    for (DexEncodedField field : holder.fields()) {
-      if (field.isStatic()
-          && field.isFinal()
-          && field.hasExplicitStaticValue()
-          && field.getType().isIntType()) {
-        appView
-            .reporter()
-            .warning(
-                new FinalRClassEntriesWithOptimizedShrinkingDiagnostic(
-                    holder.origin, field.getReference()));
-      }
-    }
-  }
-
   private void analyzeClassInitializer(
       RClassFieldToValueStore.Builder rClassValueBuilder, ProgramMethod programClassInitializer) {
     IRCode code = programClassInitializer.buildIR(appView, MethodConversionOptions.nonConverting());
 
-    // We handle two cases:
-    //  - Simple integer field assigments.
-    //  - Assigments of integer arrays to fields.
     for (StaticPut staticPut : code.<StaticPut>instructions(Instruction::isStaticPut)) {
       Value value = staticPut.value();
       if (value.isPhi()) {
@@ -204,7 +318,7 @@
       } else if (definition.isNewArrayFilled()) {
         values = new IntArrayList();
         for (Value inValue : definition.asNewArrayFilled().inValues()) {
-          if (value.isPhi()) {
+          if (inValue.isPhi()) {
             continue;
           }
           Instruction valueDefinition = inValue.definition;
@@ -231,14 +345,14 @@
         IntList values = new IntArrayList(1);
         values.add(staticValue.asDexValueInt().getValue());
         staticField.setStaticValue(
-            DexValueResourceNumber.create(staticValue.asDexValueInt().value));
+            DexValue.DexValueResourceNumber.create(staticValue.asDexValueInt().value));
         rClassValueBuilder.addMapping(staticField.getReference(), values);
       }
     }
   }
 
   private static class RClassFieldToValueStore {
-    private Map<DexField, IntList> valueMapping;
+    private final Map<DexField, IntList> valueMapping;
 
     private RClassFieldToValueStore(Map<DexField, IntList> valueMapping) {
       this.valueMapping = valueMapping;
diff --git a/src/resourceshrinker/java/com/android/tools/r8/resourceshrinker/ResourceShrinkerState.java b/src/resourceshrinker/java/com/android/tools/r8/resourceshrinker/ResourceShrinkerState.java
index a14a3b8..80afae1 100644
--- a/src/resourceshrinker/java/com/android/tools/r8/resourceshrinker/ResourceShrinkerState.java
+++ b/src/resourceshrinker/java/com/android/tools/r8/resourceshrinker/ResourceShrinkerState.java
@@ -73,8 +73,7 @@
   private final ShrinkerDebugReporter shrinkerDebugReporter;
   private final boolean enableXmlInlining;
   private final boolean enableManifestPruning;
-  private ClassReferenceCallback enqueuerCallback;
-  private MethodReferenceCallback methodCallback;
+
   private Map<Integer, Set<String>> resourceIdToXmlFiles;
   private Set<String> packageNames;
   private final Set<String> seenNoneClassValues = new HashSet<>();
@@ -92,13 +91,9 @@
   private static final Set<String> SPECIAL_APPLICATION_ATTRIBUTES =
       ImmutableSet.of("backupAgent", "appComponentFactory", "zygotePreloadName");
 
-  @FunctionalInterface
-  public interface ClassReferenceCallback {
+  public interface ResourceShrinkerCallback {
     boolean tryClass(String possibleClass, String xmlFilePath, boolean markAsLive);
-  }
 
-  @FunctionalInterface
-  public interface MethodReferenceCallback {
     void tryMethod(String methodName, String xmlFilePath);
   }
 
@@ -114,7 +109,7 @@
     this.enableManifestPruning = enableManifestPruning;
   }
 
-  public void trace(int id, String reachableFrom) {
+  public void trace(int id, String reachableFrom, ResourceShrinkerCallback callback) {
     if (!seenResourceIds.add(id)) {
       return;
     }
@@ -128,11 +123,11 @@
     reachabilityMap.compute(
         resource, (r, v) -> v == null || v.compareTo(reachableFrom) > 0 ? reachableFrom : v);
     ResourceUsageModel.markReachable(resource);
-    traceXmlForResourceId(id);
+    traceXmlForResourceId(id, callback);
     if (resource.references != null) {
       for (Resource reference : resource.references) {
         if (!reference.isReachable()) {
-          trace(reference.value, resource.toString());
+          trace(reference.value, resource.toString(), callback);
         }
       }
     }
@@ -142,7 +137,7 @@
     return getR8ResourceShrinkerModel().getResourceStore().getResource(resourceId) != null;
   }
 
-  public void traceKeepXmlAndManifest() {
+  public void traceKeepXmlAndManifest(ResourceShrinkerCallback callback) {
     // We start by building the root set of all keep/discard rules to find those pinned resources
     // before marking additional resources in the trace.
     // We then explicitly trace those resources to transitively get the full set of reachable
@@ -156,22 +151,12 @@
     r8ResourceShrinkerModel
         .getResourceStore()
         .processToolsAttributes()
-        .forEach(resource -> trace(resource.value, "keep xml file"));
+        .forEach(resource -> trace(resource.value, "keep xml file", callback));
     for (Supplier<InputStream> manifestProvider : manifestProviders) {
-      traceXml("AndroidManifest.xml", manifestProvider.get(), ids -> {});
+      traceXml("AndroidManifest.xml", manifestProvider.get(), ids -> {}, callback);
     }
   }
 
-  public void setEnqueuerCallback(ClassReferenceCallback enqueuerCallback) {
-    assert this.enqueuerCallback == null;
-    this.enqueuerCallback = enqueuerCallback;
-  }
-
-  public void setEnqueuerMethodCallback(MethodReferenceCallback methodCallback) {
-    assert this.methodCallback == null;
-    this.methodCallback = methodCallback;
-  }
-
   private synchronized Set<String> getPackageNames() {
     // TODO(b/325888516): Consider only doing this for the package corresponding to the current
     // feature.
@@ -312,15 +297,15 @@
     return resEntriesToKeep.build();
   }
 
-  private void traceXmlForResourceId(int id) {
+  private void traceXmlForResourceId(int id, ResourceShrinkerCallback callback) {
     Set<String> xmlFiles = getResourceIdToXmlFiles().get(id);
     if (xmlFiles != null) {
       for (String xmlFile : xmlFiles) {
         InputStream inputStream = xmlFileProviders.get(xmlFile).get();
         Resource resource = r8ResourceShrinkerModel.getResourceStore().getResource(id);
-        traceXml(xmlFile, inputStream, inlinedIds -> pruneModel(resource, inlinedIds));
+        traceXml(xmlFile, inputStream, inlinedIds -> pruneModel(resource, inlinedIds), callback);
         if (duplicatedResFolderEntries.contains(xmlFile)) {
-          traceDuplicatedXmlFileIds(id, xmlFile);
+          traceDuplicatedXmlFileIds(id, xmlFile, callback);
         }
       }
     }
@@ -333,18 +318,22 @@
     }
   }
 
-  private void traceDuplicatedXmlFileIds(int currentId, String xmlFile) {
+  private void traceDuplicatedXmlFileIds(
+      int currentId, String xmlFile, ResourceShrinkerCallback callback) {
     for (Map.Entry<Integer, Set<String>> entry : getResourceIdToXmlFiles().entrySet()) {
       if (entry.getValue().contains(xmlFile)) {
         if (entry.getKey() != currentId) {
-          trace(entry.getKey(), "Duplicated xmlfile " + xmlFile);
+          trace(entry.getKey(), "Duplicated xmlfile " + xmlFile, callback);
         }
       }
     }
   }
 
   private void traceXml(
-      String xmlFile, InputStream inputStream, Consumer<IntSet> inlinedIdsConsumer) {
+      String xmlFile,
+      InputStream inputStream,
+      Consumer<IntSet> inlinedIdsConsumer,
+      ResourceShrinkerCallback callback) {
     try {
       XmlNode xmlNode;
       if (changedXmlFiles.containsKey(xmlFile)) {
@@ -358,12 +347,12 @@
           changedXmlFiles.put(xmlFile, xmlNode.toByteArray());
         }
       }
-      visitNode(xmlNode, xmlFile, null);
+      visitNode(xmlNode, xmlFile, null, callback);
       // Ensure that we trace the transitive reachable ids, without us having to iterate all
       // resources for the reachable marker.
       ProtoAndroidManifestUsageRecorderKt.recordUsagesFromNode(xmlNode, r8ResourceShrinkerModel)
           .iterator()
-          .forEachRemaining(resource -> trace(resource.value, xmlFile));
+          .forEachRemaining(resource -> trace(resource.value, xmlFile, callback));
     } catch (IOException e) {
       errorHandler.apply(e);
     }
@@ -410,21 +399,26 @@
     return changedChildren;
   }
 
-  private void tryEnqueuerOnString(String possibleClass, String xmlName) {
+  private void tryEnqueuerOnString(
+      String possibleClass, String xmlName, ResourceShrinkerCallback callback) {
     // There are a lot of xml tags and attributes that are evaluated over and over, if it is
     // not a class, ignore it.
     if (seenNoneClassValues.contains(possibleClass)) {
       return;
     }
-    if (!enqueuerCallback.tryClass(possibleClass, xmlName, true)) {
+    if (!callback.tryClass(possibleClass, xmlName, true)) {
       seenNoneClassValues.add(possibleClass);
     }
   }
 
-  private void visitNode(XmlNode xmlNode, String xmlName, String manifestPackageName) {
+  private void visitNode(
+      XmlNode xmlNode,
+      String xmlName,
+      String manifestPackageName,
+      ResourceShrinkerCallback callback) {
     XmlElement element = xmlNode.getElement();
     String xmlElementName = element.getName();
-    tryEnqueuerOnString(xmlElementName, xmlName);
+    tryEnqueuerOnString(xmlElementName, xmlName, callback);
 
     for (XmlAttribute xmlAttribute : element.getAttributeList()) {
       if (xmlAttribute.getName().equals("package") && xmlElementName.equals("manifest")) {
@@ -443,44 +437,48 @@
                 .anyMatch(child -> child.getElement().getName().equals("intent-filter"));
         if (isNotExported && !hasFilters) {
           String fullyQualifiedName = getFullyQualifiedName(manifestPackageName, xmlAttribute);
-          enqueuerCallback.tryClass(fullyQualifiedName, xmlName, false);
+          callback.tryClass(fullyQualifiedName, xmlName, false);
           continue;
         }
       }
       String value = xmlAttribute.getValue();
-      tryEnqueuerOnString(value, xmlName);
+      tryEnqueuerOnString(value, xmlName, callback);
       if (value.startsWith(".")) {
         // package specific names, e.g. context
-        getPackageNames().forEach(s -> tryEnqueuerOnString(s + value, xmlName));
+        getPackageNames().forEach(s -> tryEnqueuerOnString(s + value, xmlName, callback));
       }
       if (manifestPackageName != null) {
         // Manifest case
-        traceManifestSpecificValues(xmlName, manifestPackageName, xmlAttribute, element);
+        traceManifestSpecificValues(xmlName, manifestPackageName, xmlAttribute, element, callback);
       }
       if (xmlAttribute.getName().equals("onClick")
           && xmlAttribute.getNamespaceUri().equals("http://schemas.android.com/apk/res/android")) {
-        methodCallback.tryMethod(xmlAttribute.getValue(), xmlName);
+        callback.tryMethod(xmlAttribute.getValue(), xmlName);
       }
     }
     for (XmlNode node : element.getChildList()) {
-      visitNode(node, xmlName, manifestPackageName);
+      visitNode(node, xmlName, manifestPackageName, callback);
     }
   }
 
   private void traceManifestSpecificValues(
-      String xmlName, String packageName, XmlAttribute xmlAttribute, XmlElement element) {
+      String xmlName,
+      String packageName,
+      XmlAttribute xmlAttribute,
+      XmlElement element,
+      ResourceShrinkerCallback callback) {
     if (!SPECIAL_MANIFEST_ELEMENTS.contains(element.getName())) {
       return;
     }
     // All elements can have package specific name attributes pointing at classes.
     if (xmlAttribute.getName().equals("name")) {
-      tryEnqueuerOnString(getFullyQualifiedName(packageName, xmlAttribute), xmlName);
+      tryEnqueuerOnString(getFullyQualifiedName(packageName, xmlAttribute), xmlName, callback);
     }
     // Application elements have multiple special case attributes, where the value is potentially
     // a class name (unqualified).
     if (element.getName().equals("application")) {
       if (SPECIAL_APPLICATION_ATTRIBUTES.contains(xmlAttribute.getName())) {
-        tryEnqueuerOnString(getFullyQualifiedName(packageName, xmlAttribute), xmlName);
+        tryEnqueuerOnString(getFullyQualifiedName(packageName, xmlAttribute), xmlName, callback);
       }
     }
   }
@@ -552,8 +550,6 @@
   }
 
   public void enqueuerDone(boolean isFinalTreeshaking) {
-    enqueuerCallback = null;
-    methodCallback = null;
     seenResourceIds.clear();
     if (!isFinalTreeshaking) {
       // After final tree shaking we will need the reachability bits to decide what to write out