Support resource shrinking in partial with D8 R classes

This adds resource id tracing of the R classes in the D8 part.

Bug: b/388746233
Change-Id: I2f855fea039dc65cff3f7b96a295b0b580361157
diff --git a/src/main/java/com/android/tools/r8/R8Partial.java b/src/main/java/com/android/tools/r8/R8Partial.java
index 3d52ede..ea253eb 100644
--- a/src/main/java/com/android/tools/r8/R8Partial.java
+++ b/src/main/java/com/android/tools/r8/R8Partial.java
@@ -5,6 +5,7 @@
 
 import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
 
+import com.android.build.shrinker.usages.R8ResourceShrinker;
 import com.android.tools.r8.DexIndexedConsumer.ArchiveConsumer;
 import com.android.tools.r8.DexIndexedConsumer.ForwardingConsumer;
 import com.android.tools.r8.StringConsumer.FileConsumer;
@@ -21,6 +22,8 @@
 import com.android.tools.r8.partial.R8PartialInputToDumpFlags;
 import com.android.tools.r8.partial.R8PartialR8Result;
 import com.android.tools.r8.partial.R8PartialTraceReferencesResult;
+import com.android.tools.r8.partial.R8PartialTraceResourcesResult;
+import com.android.tools.r8.partial.ResourceTracingCallback;
 import com.android.tools.r8.synthesis.SyntheticItems.GlobalSyntheticsStrategy;
 import com.android.tools.r8.tracereferences.TraceReferencesBridge;
 import com.android.tools.r8.tracereferences.TraceReferencesCommand;
@@ -29,6 +32,7 @@
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.AndroidAppConsumers;
 import com.android.tools.r8.utils.ExceptionUtils;
+import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.ForwardingDiagnosticsHandler;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ThreadUtils;
@@ -38,6 +42,7 @@
 import com.google.common.io.ByteStreams;
 import java.io.IOException;
 import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.HashSet;
 import java.util.Set;
 import java.util.concurrent.ExecutorService;
@@ -83,8 +88,10 @@
 
     R8PartialInput input = runProcessInputStep(app, timing);
     R8PartialD8DexResult d8DexResult = runD8DexStep(input, executor);
+    R8PartialTraceResourcesResult resourcesTraceResult = runTraceResourcesStep(d8DexResult);
     R8PartialTraceReferencesResult traceReferencesResult = runTraceReferencesStep(input);
-    R8PartialR8Result r8Result = runR8PartialStep(input, traceReferencesResult, executor);
+    R8PartialR8Result r8Result =
+        runR8PartialStep(input, traceReferencesResult, resourcesTraceResult, executor);
     runD8MergeStep(input, d8DexResult, r8Result, executor);
 
     // Feed the data resource output by R8 to the output consumer. Keeping this at the end after the
@@ -93,6 +100,21 @@
     timing.end();
   }
 
+  private R8PartialTraceResourcesResult runTraceResourcesStep(R8PartialD8DexResult d8DexResult)
+      throws IOException {
+    // TODO(b/390135529): Consider tracing these in the enqueuer of R8.
+    ResourceTracingCallback resourceTracingCallback = new ResourceTracingCallback();
+    ZipUtils.iter(
+        d8DexResult.getOutputPath(),
+        (entry, input) -> {
+          if (FileUtils.isDexFile(Paths.get(entry.getName()))) {
+            R8ResourceShrinker.runResourceShrinkerAnalysis(
+                input.readAllBytes(), d8DexResult.getOutputPath(), resourceTracingCallback);
+          }
+        });
+    return new R8PartialTraceResourcesResult(resourceTracingCallback.getPotentialIds());
+  }
+
   private R8PartialInput runProcessInputStep(AndroidApp androidApp, Timing timing)
       throws IOException {
     // Create a dump of the compiler input.
@@ -201,6 +223,7 @@
   private R8PartialR8Result runR8PartialStep(
       R8PartialInput input,
       R8PartialTraceReferencesResult traceReferencesResult,
+      R8PartialTraceResourcesResult resourcesTraceResult,
       ExecutorService executor)
       throws IOException {
     // Compile R8 input with R8 using the keep rules from trace references.
@@ -247,6 +270,7 @@
       r8Options.androidResourceProvider = options.androidResourceProvider;
       r8Options.androidResourceConsumer = options.androidResourceConsumer;
       r8Options.resourceShrinkerConfiguration = options.resourceShrinkerConfiguration;
+      r8Options.d8TracedResourceIDs = resourcesTraceResult.getResourceIdsToTrace();
     }
     R8.runInternal(r8App, r8Options, executor);
     if (r8OutputAppConsumer != null) {
diff --git a/src/main/java/com/android/tools/r8/partial/R8PartialTraceResourcesResult.java b/src/main/java/com/android/tools/r8/partial/R8PartialTraceResourcesResult.java
new file mode 100644
index 0000000..7c9a854
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/partial/R8PartialTraceResourcesResult.java
@@ -0,0 +1,19 @@
+// 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.partial;
+
+import it.unimi.dsi.fastutil.ints.IntSet;
+
+public class R8PartialTraceResourcesResult {
+
+  private final IntSet resourceIdsToTrace;
+
+  public R8PartialTraceResourcesResult(IntSet resourceIdsToTrace) {
+    this.resourceIdsToTrace = resourceIdsToTrace;
+  }
+
+  public IntSet getResourceIdsToTrace() {
+    return resourceIdsToTrace;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/partial/ResourceTracingCallback.java b/src/main/java/com/android/tools/r8/partial/ResourceTracingCallback.java
new file mode 100644
index 0000000..d5db2c7
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/partial/ResourceTracingCallback.java
@@ -0,0 +1,46 @@
+// 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.partial;
+
+import com.android.build.shrinker.usages.AnalysisCallback;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.utils.DescriptorUtils;
+import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
+import it.unimi.dsi.fastutil.ints.IntSet;
+
+public class ResourceTracingCallback implements AnalysisCallback {
+
+  private final IntSet potentialIds = new IntOpenHashSet();
+
+  public IntSet getPotentialIds() {
+    return potentialIds;
+  }
+
+  @Override
+  public boolean shouldProcess(String internalName) {
+    // Only process R classes.
+    return DescriptorUtils.isRClassDescriptor(
+        DescriptorUtils.internalNameToDescriptor(internalName));
+  }
+
+  @Override
+  public void referencedInt(int value) {
+    potentialIds.add(value);
+  }
+
+  @Override
+  public void referencedString(String value) {}
+
+  @Override
+  public void referencedStaticField(String internalName, String fieldName) {}
+
+  @Override
+  public void referencedMethod(String internalName, String methodName, String methodDescriptor) {}
+
+  @Override
+  public void startMethodVisit(MethodReference methodReference) {}
+
+  @Override
+  public void endMethodVisit(MethodReference methodReference) {}
+}
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 a2b0b80..6348333 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -3936,7 +3936,7 @@
     enqueueAllIfNotShrinking();
     timing.end();
     timing.begin("Trace");
-    traceManifests(timing);
+    traceManifestsAndRoots(timing);
     trace(executorService, timing);
     timing.end();
     options.reporter.failIfPendingErrors();
@@ -3969,10 +3969,13 @@
     return result;
   }
 
-  private void traceManifests(Timing timing) {
+  private void traceManifestsAndRoots(Timing timing) {
     if (options.isOptimizedResourceShrinking()) {
       timing.begin("Trace AndroidManifest.xml files");
       appView.getResourceShrinkerState().traceKeepXmlAndManifest();
+      for (int d8TracedResourceID : options.d8TracedResourceIDs) {
+        appView.getResourceShrinkerState().trace(d8TracedResourceID, "Non shrunken dex code");
+      }
       timing.end();
     }
   }
diff --git a/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java b/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
index 7d8e995..d6ae1ac 100644
--- a/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
@@ -224,6 +224,21 @@
   }
 
   /**
+   * Convert an ASM internal name to a class type descriptor
+   *
+   * @param internalName The internal name string
+   * @return type descriptor
+   */
+  public static String internalNameToDescriptor(String internalName) {
+    switch (internalName.charAt(0)) {
+      case '[':
+        return internalName;
+      default:
+        return new StringBuilder("L").append(internalName).append(";").toString();
+    }
+  }
+
+  /**
    * Convert a descriptor to a classifier in Kotlin metadata
    * @param descriptor like "Lorg/foo/bar/Baz$Nested;"
    * @return className "org/foo/bar/Baz.Nested"
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 fca7ee0..b2044f1 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -116,6 +116,8 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
+import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
+import it.unimi.dsi.fastutil.ints.IntSet;
 import java.io.IOException;
 import java.nio.file.Paths;
 import java.util.ArrayList;
@@ -197,6 +199,7 @@
   public AndroidResourceProvider androidResourceProvider = null;
   public AndroidResourceConsumer androidResourceConsumer = null;
   public List<String> androidResourceProguardMapStrings = null;
+  public IntSet d8TracedResourceIDs = new IntOpenHashSet();
 
   public ResourceShrinkerConfiguration resourceShrinkerConfiguration =
       ResourceShrinkerConfiguration.DEFAULT_CONFIGURATION;
diff --git a/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkingInPartialR8Test.java b/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkingInPartialR8Test.java
index 9784553..210b12f 100644
--- a/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkingInPartialR8Test.java
+++ b/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkingInPartialR8Test.java
@@ -63,14 +63,13 @@
         .compile()
         .inspectShrunkenResources(
             resourceTableInspector -> {
-              // TODO(b/388746233): We should still trace the resources when we have the R class
-              // in D8 code.
-              resourceTableInspector.assertDoesNotContainResourceWithName(
+              resourceTableInspector.assertContainsResourceWithName(
                   "string", "referencedFromD8Code");
-              resourceTableInspector.assertDoesNotContainResourceWithName(
+              resourceTableInspector.assertContainsResourceWithName(
                   "string", "referencedFromR8Code");
-              resourceTableInspector.assertDoesNotContainResourceWithName(
-                  "string", "unused_string");
+              // The R class is in the D8 part of the code, so we keep all entries, even
+              // unreferenced fields (since the field is still there)
+              resourceTableInspector.assertContainsResourceWithName("string", "unused_string");
             })
         .run(parameters.getRuntime(), InR8.class)
         .assertSuccess();
@@ -79,11 +78,16 @@
   private R8PartialTestBuilder getR8PartialTestBuilder(boolean rClassInD8) throws Exception {
     return testForR8Partial(parameters.getBackend())
         .setMinApi(parameters)
-        .addProgramClasses(InR8.class, InD8.class)
-        .addAndroidResources(getTestResources(temp))
-        .setDefaultIncludeAll()
+        .addR8IncludedClasses(InR8.class)
         .addR8ExcludedClasses(InD8.class)
-        .applyIf(rClassInD8, b -> b.addR8ExcludedClasses(R.string.class))
+        .addAndroidResources(getTestResources(temp))
+        .enableOptimizedShrinking()
+        .addR8ExcludedClasses(InD8.class)
+        .applyIf(
+            rClassInD8,
+            // These classes are already added as program classes by the resource setup.
+            b -> b.addR8ExcludedClasses(false, R.string.class),
+            b -> b.addR8IncludedClasses(false, R.string.class))
         .addKeepMainRule(InR8.class);
   }
 
diff --git a/src/test/java/com/android/tools/r8/partial/PartialCompilationWithDefaultInterfaceMethodTest.java b/src/test/java/com/android/tools/r8/partial/PartialCompilationWithDefaultInterfaceMethodTest.java
index ee97ab3..9d07e01 100644
--- a/src/test/java/com/android/tools/r8/partial/PartialCompilationWithDefaultInterfaceMethodTest.java
+++ b/src/test/java/com/android/tools/r8/partial/PartialCompilationWithDefaultInterfaceMethodTest.java
@@ -30,7 +30,6 @@
     // TODO(b/388763735): Enable for all API levels.
     assumeTrue(parameters.canUseDefaultAndStaticInterfaceMethods());
     testForR8Partial(parameters.getBackend())
-        .addInnerClasses(getClass())
         .addR8IncludedClasses(I.class, J.class)
         .addR8ExcludedClasses(Main.class, A.class)
         .setMinApi(parameters)
diff --git a/src/test/testbase/java/com/android/tools/r8/R8PartialTestBuilder.java b/src/test/testbase/java/com/android/tools/r8/R8PartialTestBuilder.java
index a6bc7fa..8f45b65 100644
--- a/src/test/testbase/java/com/android/tools/r8/R8PartialTestBuilder.java
+++ b/src/test/testbase/java/com/android/tools/r8/R8PartialTestBuilder.java
@@ -24,7 +24,6 @@
 
   private final ArrayList<Class<?>> includedClasses = new ArrayList<>();
   private final ArrayList<Class<?>> excludedClasses = new ArrayList<>();
-  private boolean defaultIncludeAll = false;
   private R8PartialCompilationConfiguration r8PartialConfiguration =
       R8PartialCompilationConfiguration.disabledConfiguration();
 
@@ -86,22 +85,33 @@
     return self();
   }
 
-  public R8PartialTestBuilder setDefaultIncludeAll() {
-    this.defaultIncludeAll = true;
-    return self();
+  public R8PartialTestBuilder addR8IncludedClasses(Class<?>... classes) {
+    return addR8IncludedClasses(true, classes);
   }
 
-  public R8PartialTestBuilder addR8IncludedClasses(Class<?>... classes) {
+  public R8PartialTestBuilder addR8IncludedClasses(
+      boolean addAsProgramClasses, Class<?>... classes) {
     assert r8PartialConfiguration.equals(R8PartialCompilationConfiguration.disabledConfiguration())
         : "Overwriting configuration...?";
     Collections.addAll(includedClasses, classes);
+    if (addAsProgramClasses) {
+      addProgramClasses(classes);
+    }
     return self();
   }
 
   public R8PartialTestBuilder addR8ExcludedClasses(Class<?>... classes) {
+    return addR8ExcludedClasses(true, classes);
+  }
+
+  public R8PartialTestBuilder addR8ExcludedClasses(
+      boolean addAsProgramClasses, Class<?>... classes) {
     assert r8PartialConfiguration.equals(R8PartialCompilationConfiguration.disabledConfiguration())
         : "Overwriting configuration...?";
     Collections.addAll(excludedClasses, classes);
+    if (addAsProgramClasses) {
+      addProgramClasses(classes);
+    }
     return self();
   }
 
@@ -112,12 +122,7 @@
     }
     R8PartialCompilationConfiguration.Builder partialBuilder =
         R8PartialCompilationConfiguration.builder();
-    if (defaultIncludeAll) {
-      assert includedClasses.isEmpty();
-      partialBuilder.includeAll();
-    } else {
-      partialBuilder.includeClasses(includedClasses);
-    }
+    partialBuilder.includeClasses(includedClasses);
     partialBuilder.excludeClasses(excludedClasses);
     return partialBuilder.build();
   }