Reapply "Initial setup for doing legacy resource shrinking in R8"

This reverts commit 8ea20ac4540f3800f13f5f54c52d3456bf9409fe.

Bug: 305892375
Bug: 287398085

Change-Id: If6e6ee396cd6dcee4619b733d94f6dd456e4b7b6
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/LegacyResourceShrinker.java b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/LegacyResourceShrinker.java
new file mode 100644
index 0000000..9fd4163
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/LegacyResourceShrinker.java
@@ -0,0 +1,270 @@
+// Copyright (c) 2023, 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.build.shrinker.r8integration;
+
+import static java.nio.charset.StandardCharsets.UTF_16BE;
+import static java.nio.charset.StandardCharsets.UTF_16LE;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+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.graph.ProtoResourcesGraphBuilder;
+import com.android.build.shrinker.graph.ResFolderFileTree;
+import com.android.build.shrinker.r8integration.R8ResourceShrinkerState.R8ResourceShrinkerModel;
+import com.android.build.shrinker.usages.DexFileAnalysisCallback;
+import com.android.build.shrinker.usages.ProtoAndroidManifestUsageRecorderKt;
+import com.android.build.shrinker.usages.R8ResourceShrinker;
+import com.android.build.shrinker.usages.ToolsAttributeUsageRecorderKt;
+import com.android.ide.common.resources.usage.ResourceStore;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.StringReader;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.xml.parsers.ParserConfigurationException;
+import org.jetbrains.annotations.NotNull;
+import org.xml.sax.SAXException;
+
+public class LegacyResourceShrinker {
+  private final Map<Integer, byte[]> dexInputs;
+  private final List<PathAndBytes> resFolderInputs;
+  private final List<PathAndBytes> xmlInputs;
+  private final PathAndBytes manifest;
+  private final PathAndBytes resourceTable;
+
+  public static class Builder {
+
+    private final Map<Integer, byte[]> dexInputs = new HashMap<>();
+    private final List<PathAndBytes> resFolderInputs = new ArrayList<>();
+    private final List<PathAndBytes> xmlInputs = new ArrayList<>();
+
+    private PathAndBytes manifest;
+    private PathAndBytes resourceTable;
+
+    private Builder() {}
+
+    public Builder setManifest(Path path, byte[] bytes) {
+      this.manifest = new PathAndBytes(bytes, path);
+      return this;
+    }
+
+    public Builder setResourceTable(Path path, byte[] bytes) {
+      this.resourceTable = new PathAndBytes(bytes, path);
+      return this;
+    }
+
+    public Builder addDexInput(int index, byte[] bytes) {
+      dexInputs.put(index, bytes);
+      return this;
+    }
+
+    public Builder addResFolderInput(Path path, byte[] bytes) {
+      resFolderInputs.add(new PathAndBytes(bytes, path));
+      return this;
+    }
+
+    public Builder addXmlInput(Path path, byte[] bytes) {
+      xmlInputs.add(new PathAndBytes(bytes, path));
+      return this;
+    }
+
+    public LegacyResourceShrinker build() {
+      assert manifest != null && resourceTable != null;
+      return new LegacyResourceShrinker(
+          dexInputs, resFolderInputs, manifest, resourceTable, xmlInputs);
+    }
+  }
+
+  private LegacyResourceShrinker(
+      Map<Integer, byte[]> dexInputs,
+      List<PathAndBytes> resFolderInputs,
+      PathAndBytes manifest,
+      PathAndBytes resourceTable,
+      List<PathAndBytes> xmlInputs) {
+    this.dexInputs = dexInputs;
+    this.resFolderInputs = resFolderInputs;
+    this.manifest = manifest;
+    this.resourceTable = resourceTable;
+    this.xmlInputs = xmlInputs;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public ShrinkerResult run() throws IOException, ParserConfigurationException, SAXException {
+    R8ResourceShrinkerModel model = new R8ResourceShrinkerModel(NoDebugReporter.INSTANCE, false);
+    ResourceTable loadedResourceTable = ResourceTable.parseFrom(resourceTable.bytes);
+    model.instantiateFromResourceTable(loadedResourceTable);
+    for (Entry<Integer, 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_classes" + entry.getKey() + ".dex");
+      R8ResourceShrinker.runResourceShrinkerAnalysis(
+          entry.getValue(), inMemoryR8, new DexFileAnalysisCallback(inMemoryR8, model));
+    }
+    ProtoAndroidManifestUsageRecorderKt.recordUsagesFromNode(
+        XmlNode.parseFrom(manifest.bytes), model);
+    for (PathAndBytes xmlInput : xmlInputs) {
+      ToolsAttributeUsageRecorderKt.processRawXml(getUtfReader(xmlInput.getBytes()), model);
+    }
+    new ProtoResourcesGraphBuilder(
+            new ResFolderFileTree() {
+              Map<String, PathAndBytes> pathToBytes =
+                  new ImmutableMap.Builder<String, PathAndBytes>()
+                      .putAll(
+                          xmlInputs.stream()
+                              .collect(Collectors.toMap(PathAndBytes::getPathWithoutRes, a -> a)))
+                      .putAll(
+                          resFolderInputs.stream()
+                              .collect(Collectors.toMap(PathAndBytes::getPathWithoutRes, a -> a)))
+                      .build();
+
+              @Override
+              public byte[] getEntryByName(@NotNull String pathInRes) {
+                return pathToBytes.get(pathInRes).getBytes();
+              }
+            },
+            unused -> loadedResourceTable)
+        .buildGraph(model);
+    ResourceStore resourceStore = model.getResourceStore();
+    resourceStore.processToolsAttributes();
+    model.keepPossiblyReferencedResources();
+    ImmutableSet.Builder<String> resEntriesToKeep = new ImmutableSet.Builder<>();
+    for (PathAndBytes xmlInput : Iterables.concat(xmlInputs, resFolderInputs)) {
+      if (ResourceShrinkerImplKt.isJarPathReachable(resourceStore, xmlInput.path.toString())) {
+        resEntriesToKeep.add(xmlInput.path.toString());
+      }
+    }
+    List<Integer> resourceIdsToRemove =
+        model.getResourceStore().getResources().stream()
+            .filter(r -> !r.isReachable())
+            .map(r -> r.value)
+            .collect(Collectors.toList());
+    ResourceTable shrunkenResourceTable =
+        ResourceTableUtilKt.nullOutEntriesWithIds(loadedResourceTable, resourceIdsToRemove);
+    return new ShrinkerResult(resEntriesToKeep.build(), shrunkenResourceTable.toByteArray());
+  }
+
+  // Lifted from com/android/utils/XmlUtils.java which we can't easily update internal dependency
+  // for.
+  /**
+   * Returns a character reader for the given bytes, which must be a UTF encoded file.
+   *
+   * <p>The reader does not need to be closed by the caller (because the file is read in full in one
+   * shot and the resulting array is then wrapped in a byte array input stream, which does not need
+   * to be closed.)
+   */
+  public static Reader getUtfReader(byte[] bytes) throws IOException {
+    int length = bytes.length;
+    if (length == 0) {
+      return new StringReader("");
+    }
+
+    switch (bytes[0]) {
+      case (byte) 0xEF:
+        {
+          if (length >= 3 && bytes[1] == (byte) 0xBB && bytes[2] == (byte) 0xBF) {
+            // UTF-8 BOM: EF BB BF: Skip it
+            return new InputStreamReader(new ByteArrayInputStream(bytes, 3, length - 3), UTF_8);
+          }
+          break;
+        }
+      case (byte) 0xFE:
+        {
+          if (length >= 2 && bytes[1] == (byte) 0xFF) {
+            // UTF-16 Big Endian BOM: FE FF
+            return new InputStreamReader(new ByteArrayInputStream(bytes, 2, length - 2), UTF_16BE);
+          }
+          break;
+        }
+      case (byte) 0xFF:
+        {
+          if (length >= 2 && bytes[1] == (byte) 0xFE) {
+            if (length >= 4 && bytes[2] == (byte) 0x00 && bytes[3] == (byte) 0x00) {
+              // UTF-32 Little Endian BOM: FF FE 00 00
+              return new InputStreamReader(
+                  new ByteArrayInputStream(bytes, 4, length - 4), "UTF-32LE");
+            }
+
+            // UTF-16 Little Endian BOM: FF FE
+            return new InputStreamReader(new ByteArrayInputStream(bytes, 2, length - 2), UTF_16LE);
+          }
+          break;
+        }
+      case (byte) 0x00:
+        {
+          if (length >= 4
+              && bytes[0] == (byte) 0x00
+              && bytes[1] == (byte) 0x00
+              && bytes[2] == (byte) 0xFE
+              && bytes[3] == (byte) 0xFF) {
+            // UTF-32 Big Endian BOM: 00 00 FE FF
+            return new InputStreamReader(
+                new ByteArrayInputStream(bytes, 4, length - 4), "UTF-32BE");
+          }
+          break;
+        }
+    }
+
+    // No byte order mark: Assume UTF-8 (where the BOM is optional).
+    return new InputStreamReader(new ByteArrayInputStream(bytes), UTF_8);
+  }
+
+  public static class ShrinkerResult {
+    private final Set<String> resFolderEntriesToKeep;
+    private final byte[] resourceTableInProtoFormat;
+
+    public ShrinkerResult(Set<String> resFolderEntriesToKeep, byte[] resourceTableInProtoFormat) {
+      this.resFolderEntriesToKeep = resFolderEntriesToKeep;
+      this.resourceTableInProtoFormat = resourceTableInProtoFormat;
+    }
+
+    public byte[] getResourceTableInProtoFormat() {
+      return resourceTableInProtoFormat;
+    }
+
+    public Set<String> getResFolderEntriesToKeep() {
+      return resFolderEntriesToKeep;
+    }
+  }
+
+  private static class PathAndBytes {
+    private final byte[] bytes;
+    private final Path path;
+
+    private PathAndBytes(byte[] bytes, Path path) {
+      this.bytes = bytes;
+      this.path = path;
+    }
+
+    public Path getPath() {
+      return path;
+    }
+
+    public String getPathWithoutRes() {
+      assert path.toString().startsWith("res/");
+      return path.toString().substring(4);
+    }
+
+    public byte[] getBytes() {
+      return bytes;
+    }
+  }
+}