Add resource logging api, and report resource logs

The current implementation is consistent with the old resource shrinker for legacy mode

Bug: b/360284025
Bug: b/360284664
Change-Id: Idfce812bc92f357c04e2b5944d5db43253fd970a
Fixes: 360284025
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index f84fd22..088b1ee 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -114,6 +114,7 @@
 import com.android.tools.r8.utils.ExceptionUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.Reporter;
+import com.android.tools.r8.utils.ResourceShrinkerUtils;
 import com.android.tools.r8.utils.SelfRetraceTest;
 import com.android.tools.r8.utils.StringDiagnostic;
 import com.android.tools.r8.utils.StringUtils;
@@ -968,6 +969,9 @@
       if (options.androidResourceProguardMapStrings != null) {
         resourceShrinkerBuilder.setProguardMapStrings(options.androidResourceProguardMapStrings);
       }
+      resourceShrinkerBuilder.setShrinkerDebugReporter(
+          ResourceShrinkerUtils.shrinkerDebugReporterFromStringConsumer(
+              options.resourceShrinkerConfiguration.getDebugConsumer(), reporter));
       LegacyResourceShrinker shrinker = resourceShrinkerBuilder.build();
       ShrinkerResult shrinkerResult;
       if (options.resourceShrinkerConfiguration.isOptimizedShrinking()) {
diff --git a/src/main/java/com/android/tools/r8/ResourceShrinkerConfiguration.java b/src/main/java/com/android/tools/r8/ResourceShrinkerConfiguration.java
index 1a5b781..82137e2 100644
--- a/src/main/java/com/android/tools/r8/ResourceShrinkerConfiguration.java
+++ b/src/main/java/com/android/tools/r8/ResourceShrinkerConfiguration.java
@@ -30,14 +30,17 @@
 @KeepForApi
 public class ResourceShrinkerConfiguration {
   public static ResourceShrinkerConfiguration DEFAULT_CONFIGURATION =
-      new ResourceShrinkerConfiguration(false, true);
+      new ResourceShrinkerConfiguration(false, true, null);
 
   private final boolean optimizedShrinking;
   private final boolean preciseShrinking;
+  private final StringConsumer debugConsumer;
 
-  private ResourceShrinkerConfiguration(boolean optimizedShrinking, boolean preciseShrinking) {
+  private ResourceShrinkerConfiguration(
+      boolean optimizedShrinking, boolean preciseShrinking, StringConsumer debugConsumer) {
     this.optimizedShrinking = optimizedShrinking;
     this.preciseShrinking = preciseShrinking;
+    this.debugConsumer = debugConsumer;
   }
 
   public static Builder builder(DiagnosticsHandler handler) {
@@ -52,6 +55,10 @@
     return preciseShrinking;
   }
 
+  public StringConsumer getDebugConsumer() {
+    return debugConsumer;
+  }
+
   /**
    * Builder for constructing a ResourceShrinkerConfiguration.
    *
@@ -63,6 +70,7 @@
 
     private boolean optimizedShrinking = false;
     private boolean preciseShrinking = true;
+    private StringConsumer debugConsumer;
 
     private Builder() {}
 
@@ -82,6 +90,11 @@
       return this;
     }
 
+    public Builder setDebugConsumer(StringConsumer consumer) {
+      this.debugConsumer = consumer;
+      return this;
+    }
+
     /**
      * Disable precise shrinking.
      *
@@ -97,7 +110,7 @@
 
     /** Build and return the {@link ResourceShrinkerConfiguration} */
     public ResourceShrinkerConfiguration build() {
-      return new ResourceShrinkerConfiguration(optimizedShrinking, preciseShrinking);
+      return new ResourceShrinkerConfiguration(optimizedShrinking, preciseShrinking, debugConsumer);
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/ResourceShrinkerUtils.java b/src/main/java/com/android/tools/r8/utils/ResourceShrinkerUtils.java
index 41ad64f..74fbb46 100644
--- a/src/main/java/com/android/tools/r8/utils/ResourceShrinkerUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ResourceShrinkerUtils.java
@@ -3,21 +3,28 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.utils;
 
+import com.android.build.shrinker.NoDebugReporter;
+import com.android.build.shrinker.ShrinkerDebugReporter;
 import com.android.build.shrinker.r8integration.R8ResourceShrinkerState;
 import com.android.tools.r8.AndroidResourceInput;
+import com.android.tools.r8.DiagnosticsHandler;
 import com.android.tools.r8.FeatureSplit;
 import com.android.tools.r8.ResourceException;
+import com.android.tools.r8.StringConsumer;
 import com.android.tools.r8.graph.AppView;
 import java.io.InputStream;
 import java.util.Collection;
+import java.util.function.Supplier;
 
 public class ResourceShrinkerUtils {
 
   public static R8ResourceShrinkerState createResourceShrinkerState(AppView<?> appView) {
+    InternalOptions options = appView.options();
     R8ResourceShrinkerState state =
         new R8ResourceShrinkerState(
-            exception -> appView.reporter().fatalError(new ExceptionDiagnostic(exception)));
-    InternalOptions options = appView.options();
+            exception -> appView.reporter().fatalError(new ExceptionDiagnostic(exception)),
+            shrinkerDebugReporterFromStringConsumer(
+                options.resourceShrinkerConfiguration.getDebugConsumer(), appView.reporter()));
     if (options.resourceShrinkerConfiguration.isOptimizedShrinking()
         && options.androidResourceProvider != null) {
       try {
@@ -77,6 +84,29 @@
     }
   }
 
+  public static ShrinkerDebugReporter shrinkerDebugReporterFromStringConsumer(
+      StringConsumer consumer, DiagnosticsHandler diagnosticsHandler) {
+    if (consumer == null) {
+      return NoDebugReporter.INSTANCE;
+    }
+    return new ShrinkerDebugReporter() {
+      @Override
+      public void debug(Supplier<String> logSupplier) {
+        consumer.accept(logSupplier.get(), diagnosticsHandler);
+      }
+
+      @Override
+      public void info(Supplier<String> logProducer) {
+        consumer.accept(logProducer.get(), diagnosticsHandler);
+      }
+
+      @Override
+      public void close() throws Exception {
+        consumer.finished(diagnosticsHandler);
+      }
+    };
+  }
+
   private static InputStream wrapThrowingInputStreamResource(
       AppView<?> appView, AndroidResourceInput androidResource) {
     try {
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/ShrinkerDebugReporter.kt b/src/resourceshrinker/java/com/android/build/shrinker/ShrinkerDebugReporter.kt
index b592bb8..bcb6af3 100644
--- a/src/resourceshrinker/java/com/android/build/shrinker/ShrinkerDebugReporter.kt
+++ b/src/resourceshrinker/java/com/android/build/shrinker/ShrinkerDebugReporter.kt
@@ -18,16 +18,17 @@
 
 import java.io.File
 import java.io.PrintWriter
+import java.util.function.Supplier
 
 interface ShrinkerDebugReporter : AutoCloseable {
-    fun debug(f: () -> String)
-    fun info(f: () -> String)
+    fun debug(f: Supplier<String>)
+    fun info(f: Supplier<String>)
 }
 
 object NoDebugReporter : ShrinkerDebugReporter {
-    override fun debug(f: () -> String) = Unit
+    override fun debug(f: Supplier<String>) = Unit
 
-    override fun info(f: () -> String) = Unit
+    override fun info(f: Supplier<String>) = Unit
 
     override fun close() = Unit
 }
@@ -36,12 +37,12 @@
     reportFile: File
 ) : ShrinkerDebugReporter {
     private val writer: PrintWriter = reportFile.let { PrintWriter(it) }
-    override fun debug(f: () -> String) {
-        writer.println(f())
+    override fun debug(f: Supplier<String>) {
+        writer.println(f.get())
     }
 
-    override fun info(f: () -> String) {
-        writer.println(f())
+    override fun info(f: Supplier<String>) {
+        writer.println(f.get())
     }
 
     override fun close() {
@@ -56,14 +57,14 @@
 ) : ShrinkerDebugReporter {
     private val writer: PrintWriter? = reportFile?.let { PrintWriter(it) }
 
-    override fun debug(f: () -> String) {
-        val message = f()
+     override fun debug(f: Supplier<String>) {
+        val message = f.get()
         writer?.println(message)
         logDebug(message)
     }
 
-    override fun info(f: () -> String) {
-        val message = f()
+    override fun info(f: Supplier<String>) {
+        val message = f.get()
         writer?.println(message)
         logInfo(message)
     }
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/LegacyResourceShrinker.java b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/LegacyResourceShrinker.java
index 7a82e3f..f1ba15e 100644
--- a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/LegacyResourceShrinker.java
+++ b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/LegacyResourceShrinker.java
@@ -10,9 +10,9 @@
 
 import com.android.aapt.Resources.ResourceTable;
 import com.android.aapt.Resources.XmlNode;
-import com.android.build.shrinker.NoDebugReporter;
 import com.android.build.shrinker.ResourceShrinkerImplKt;
 import com.android.build.shrinker.ResourceTableUtilKt;
+import com.android.build.shrinker.ShrinkerDebugReporter;
 import com.android.build.shrinker.graph.ProtoResourcesGraphBuilder;
 import com.android.build.shrinker.obfuscation.ProguardMappingsRecorder;
 import com.android.build.shrinker.r8integration.R8ResourceShrinkerState.R8ResourceShrinkerModel;
@@ -53,6 +53,7 @@
   private final Collection<PathAndBytes> resFolderInputs;
   private final Collection<PathAndBytes> xmlInputs;
   private List<String> proguardMapStrings;
+  private final ShrinkerDebugReporter debugReporter;
   private final List<PathAndBytes> manifest;
   private final Map<PathAndBytes, FeatureSplit> resourceTables;
 
@@ -65,6 +66,7 @@
     private final List<PathAndBytes> manifests = new ArrayList<>();
     private final Map<PathAndBytes, FeatureSplit> resourceTables = new HashMap<>();
     private List<String> proguardMapStrings;
+    private ShrinkerDebugReporter debugReporter;
 
     private Builder() {}
 
@@ -117,12 +119,18 @@
           manifests,
           resourceTables,
           xmlInputs.values(),
-          proguardMapStrings);
+          proguardMapStrings,
+          debugReporter);
     }
 
     public void setProguardMapStrings(List<String> proguardMapStrings) {
       this.proguardMapStrings = proguardMapStrings;
     }
+
+    public Builder setShrinkerDebugReporter(ShrinkerDebugReporter debugReporter) {
+      this.debugReporter = debugReporter;
+      return this;
+    }
   }
 
   private LegacyResourceShrinker(
@@ -131,13 +139,15 @@
       List<PathAndBytes> manifests,
       Map<PathAndBytes, FeatureSplit> resourceTables,
       Collection<PathAndBytes> xmlInputs,
-      List<String> proguardMapStrings) {
+      List<String> proguardMapStrings,
+      ShrinkerDebugReporter debugReporter) {
     this.dexInputs = dexInputs;
     this.resFolderInputs = resFolderInputs;
     this.manifest = manifests;
     this.resourceTables = resourceTables;
     this.xmlInputs = xmlInputs;
     this.proguardMapStrings = proguardMapStrings;
+    this.debugReporter = debugReporter;
   }
 
   public static Builder builder() {
@@ -145,7 +155,7 @@
   }
 
   public ShrinkerResult run() throws IOException, ParserConfigurationException, SAXException {
-    R8ResourceShrinkerModel model = new R8ResourceShrinkerModel(NoDebugReporter.INSTANCE, true);
+    R8ResourceShrinkerModel model = new R8ResourceShrinkerModel(debugReporter, true);
     for (PathAndBytes pathAndBytes : resourceTables.keySet()) {
       ResourceTable loadedResourceTable = ResourceTable.parseFrom(pathAndBytes.bytes);
       model.instantiateFromResourceTable(loadedResourceTable, false);
@@ -156,7 +166,7 @@
     }
     for (Entry<String, byte[]> entry : dexInputs.entrySet()) {
       // The analysis needs an origin for the dex files, synthesize an easy recognizable one.
-      Path inMemoryR8 = Paths.get("in_memory_r8_" + entry.getKey() + ".dex");
+      Path inMemoryR8 = Paths.get("in_memory_r8_" + entry.getKey());
       R8ResourceShrinker.runResourceShrinkerAnalysis(
           entry.getValue(), inMemoryR8, new DexFileAnalysisCallback(inMemoryR8, model));
     }
@@ -188,11 +198,19 @@
     ResourceStore resourceStore = model.getResourceStore();
     resourceStore.processToolsAttributes();
     model.keepPossiblyReferencedResources();
+    debugReporter.debug(model.getResourceStore()::dumpResourceModel);
     // Transitively mark the reachable resources in the model.
     // Finds unused resources in provided resources collection.
     // Marks all used resources as 'reachable' in original collection.
     List<Resource> unusedResources =
-        ResourcesUtil.findUnusedResources(model.getResourceStore().getResources(), x -> {});
+        ResourcesUtil.findUnusedResources(
+            model.getResourceStore().getResources(),
+            roots -> {
+              debugReporter.debug(() -> "The root reachable resources are:");
+              roots.forEach(root -> debugReporter.debug(() -> " " + root));
+            });
+    debugReporter.debug(() -> "Unused resources are: ");
+    unusedResources.forEach(unused -> debugReporter.debug(() -> " " + unused));
     ImmutableSet.Builder<String> resEntriesToKeep = new ImmutableSet.Builder<>();
     for (PathAndBytes xmlInput : Iterables.concat(xmlInputs, resFolderInputs)) {
       if (ResourceShrinkerImplKt.isJarPathReachable(resourceStore, xmlInput.path.toString())) {
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java
index 49f3546..2dd02e8 100644
--- a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java
+++ b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java
@@ -16,7 +16,6 @@
 import com.android.aapt.Resources.XmlAttribute;
 import com.android.aapt.Resources.XmlElement;
 import com.android.aapt.Resources.XmlNode;
-import com.android.build.shrinker.NoDebugReporter;
 import com.android.build.shrinker.ResourceShrinkerImplKt;
 import com.android.build.shrinker.ResourceShrinkerModel;
 import com.android.build.shrinker.ResourceTableUtilKt;
@@ -69,8 +68,10 @@
     boolean tryClass(String possibleClass, Origin xmlFileOrigin);
   }
 
-  public R8ResourceShrinkerState(Function<Exception, RuntimeException> errorHandler) {
-    r8ResourceShrinkerModel = new R8ResourceShrinkerModel(NoDebugReporter.INSTANCE, true);
+  public R8ResourceShrinkerState(
+      Function<Exception, RuntimeException> errorHandler,
+      ShrinkerDebugReporter shrinkerDebugReporter) {
+    r8ResourceShrinkerModel = new R8ResourceShrinkerModel(shrinkerDebugReporter, true);
     this.errorHandler = errorHandler;
   }
 
diff --git a/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkerLoggingTest.java b/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkerLoggingTest.java
new file mode 100644
index 0000000..5ed2e09
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkerLoggingTest.java
@@ -0,0 +1,190 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.androidresources;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResource;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResourceBuilder;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ResourceShrinkerLoggingTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameter(1)
+  public boolean optimized;
+
+  @Parameters(name = "{0}, optimized: {1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withDefaultDexRuntime().withAllApiLevels().build(),
+        BooleanUtils.values());
+  }
+
+  public static AndroidTestResource getTestResources(TemporaryFolder temp) throws Exception {
+    return new AndroidTestResourceBuilder()
+        .withSimpleManifestAndAppNameString()
+        .addRClassInitializeWithDefaultValues(R.string.class, R.drawable.class)
+        .build(temp);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    StringBuilder log = new StringBuilder();
+    testForR8(parameters.getBackend())
+        .setMinApi(parameters)
+        .addProgramClasses(FooBar.class)
+        .apply(
+            b ->
+                b.getBuilder()
+                    .setResourceShrinkerConfiguration(
+                        configurationBuilder -> {
+                          if (optimized) {
+                            configurationBuilder.enableOptimizedShrinkingWithR8();
+                          }
+                          configurationBuilder.setDebugConsumer(
+                              (string, handler) -> log.append(string + "\n"));
+                          return configurationBuilder.build();
+                        }))
+        .addAndroidResources(getTestResources(temp))
+        .addKeepMainRule(FooBar.class)
+        .compile()
+        .inspectShrunkenResources(
+            resourceTableInspector -> {
+              resourceTableInspector.assertContainsResourceWithName("string", "bar");
+              resourceTableInspector.assertContainsResourceWithName("string", "foo");
+              resourceTableInspector.assertContainsResourceWithName("drawable", "foobar");
+              resourceTableInspector.assertDoesNotContainResourceWithName(
+                  "string", "unused_string");
+              resourceTableInspector.assertDoesNotContainResourceWithName(
+                  "drawable", "unused_drawable");
+            })
+        .run(parameters.getRuntime(), FooBar.class)
+        .assertSuccess();
+    // TODO(b/360284664): Add (non compatible) logging for optimized shrinking
+    if (!optimized) {
+      // Consistent with the old AGP embedded shrinker
+      List<String> strings = StringUtils.splitLines(log.toString());
+      // string:bar reachable from code
+      for (String dexReachableString : ImmutableList.of("bar", "foo")) {
+        ensureDexReachableResourcesState(strings, "string", dexReachableString, true);
+        ensureResourceReachabilityState(strings, "string", dexReachableString, true);
+        ensureRootResourceState(strings, "string", dexReachableString, true);
+        ensureUnusedState(strings, "string", dexReachableString, false);
+      }
+      // The app name is only reachable from the manifest, not dex
+      ensureDexReachableResourcesState(strings, "string", "app_name", false);
+      ensureResourceReachabilityState(strings, "string", "app_name", true);
+      ensureRootResourceState(strings, "string", "app_name", true);
+      ensureUnusedState(strings, "string", "app_name", false);
+
+      ensureDexReachableResourcesState(strings, "drawable", "unused_drawable", false);
+      ensureResourceReachabilityState(strings, "drawable", "unused_drawable", false);
+      ensureRootResourceState(strings, "drawable", "unused_drawable", false);
+      ensureUnusedState(strings, "drawable", "unused_drawable", true);
+
+      ensureDexReachableResourcesState(strings, "drawable", "foobar", true);
+      ensureResourceReachabilityState(strings, "drawable", "foobar", true);
+      ensureRootResourceState(strings, "drawable", "foobar", true);
+      ensureUnusedState(strings, "drawable", "foobar", false);
+    }
+  }
+
+  private void ensureDexReachableResourcesState(
+      List<String> logStrings, String type, String name, boolean reachable) {
+    // Example line:
+    // Marking drawable:foobar:2130771968 reachable: referenced from classes.dex
+    assertEquals(
+        logStrings.stream()
+            .anyMatch(
+                s ->
+                    s.contains("Marking " + type + ":" + name)
+                        && s.contains("reachable: referenced from")),
+        reachable);
+  }
+
+  private void ensureResourceReachabilityState(
+      List<String> logStrings, String type, String name, boolean reachable) {
+    // Example line:
+    // @packagename:string/bar : reachable=true
+    assertTrue(
+        logStrings.stream()
+            .anyMatch(s -> s.contains(type + "/" + name + " : reachable=" + reachable)));
+  }
+
+  private void ensureRootResourceState(
+      List<String> logStrings, String type, String name, boolean isRoot) {
+    assertEquals(isInSection(logStrings, type, name, "The root reachable resources are:"), isRoot);
+  }
+
+  private void ensureUnusedState(
+      List<String> logStrings, String type, String name, boolean isUnused) {
+    assertEquals(isInSection(logStrings, type, name, "Unused resources are: "), isUnused);
+  }
+
+  private static boolean isInSection(
+      List<String> logStrings, String type, String name, String sectionHeader) {
+    // Example for roots
+    // "The root reachable resources are:"
+    // " drawable:foobar:2130771968"
+    boolean isInSection = false;
+    for (String logString : logStrings) {
+      if (logString.equals(sectionHeader)) {
+        isInSection = true;
+        continue;
+      }
+      if (isInSection) {
+        if (!logString.startsWith(" ")) {
+          return false;
+        }
+        if (logString.startsWith(" " + type + ":" + name)) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  public static class FooBar {
+
+    public static void main(String[] args) {
+      if (System.currentTimeMillis() == 0) {
+        System.out.println(R.drawable.foobar);
+        System.out.println(R.string.bar);
+        System.out.println(R.string.foo);
+      }
+    }
+  }
+
+  public static class R {
+
+    public static class string {
+
+      public static int bar;
+      public static int foo;
+      public static int unused_string;
+    }
+
+    public static class drawable {
+
+      public static int foobar;
+      public static int unused_drawable;
+    }
+  }
+}