Initial setup for doing legacy resource shrinking in R8

With legacy we mean do exactly like the current pipeline, which is
first dexing the app, then running resource shrinking on the output
dex and the resources.

This tries to minimize the changes to the shrinker library outside
r8integration to ease sync with studio.

This also maintains compatability with the old version of android
common used internally.

Bug: 305892375
Bug: 287398085
Change-Id: I062aad33340d1d165f8220df6b710a8b829c2b2b
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 9964d6e..e19a8ad 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -7,6 +7,9 @@
 import static com.android.tools.r8.utils.AssertionUtils.forTesting;
 import static com.android.tools.r8.utils.ExceptionUtils.unwrapExecutionException;
 
+import com.android.build.shrinker.r8integration.LegacyResourceShrinker;
+import com.android.build.shrinker.r8integration.LegacyResourceShrinker.ShrinkerResult;
+import com.android.tools.r8.DexIndexedConsumer.ForwardingConsumer;
 import com.android.tools.r8.androidapi.ApiReferenceStubber;
 import com.android.tools.r8.desugar.desugaredlibrary.DesugaredLibraryKeepRuleGenerator;
 import com.android.tools.r8.dex.ApplicationReader;
@@ -75,6 +78,7 @@
 import com.android.tools.r8.optimize.proto.ProtoNormalizer;
 import com.android.tools.r8.optimize.redundantbridgeremoval.RedundantBridgeRemover;
 import com.android.tools.r8.origin.CommandLineOrigin;
+import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.profile.art.ArtProfileCompletenessChecker;
 import com.android.tools.r8.profile.rewriting.ProfileCollectionAdditions;
 import com.android.tools.r8.repackaging.Repackaging;
@@ -105,9 +109,11 @@
 import com.android.tools.r8.synthesis.SyntheticFinalization;
 import com.android.tools.r8.synthesis.SyntheticItems;
 import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.ExceptionDiagnostic;
 import com.android.tools.r8.utils.ExceptionUtils;
 import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.ResourceTracing;
+import com.android.tools.r8.utils.Pair;
+import com.android.tools.r8.utils.Reporter;
 import com.android.tools.r8.utils.SelfRetraceTest;
 import com.android.tools.r8.utils.StringDiagnostic;
 import com.android.tools.r8.utils.StringUtils;
@@ -118,6 +124,8 @@
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.PrintStream;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
@@ -125,6 +133,8 @@
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.function.Supplier;
+import javax.xml.parsers.ParserConfigurationException;
+import org.xml.sax.SAXException;
 
 /**
  * The R8 compiler.
@@ -830,17 +840,29 @@
 
       new DesugaredLibraryKeepRuleGenerator(appView).runIfNecessary(timing);
 
+      List<Pair<Integer, byte[]>> dexFileContent = new ArrayList<>();
       if (options.androidResourceProvider != null && options.androidResourceConsumer != null) {
-        // Currently this is simply a pass through of all resources.
-        writeAndroidResources(
-            options.androidResourceProvider, options.androidResourceConsumer, appView.reporter());
+        options.programConsumer =
+            new ForwardingConsumer((DexIndexedConsumer) options.programConsumer) {
+              @Override
+              public void accept(
+                  int fileIndex,
+                  ByteDataView data,
+                  Set<String> descriptors,
+                  DiagnosticsHandler handler) {
+                dexFileContent.add(new Pair<>(fileIndex, data.copyByteData()));
+                super.accept(fileIndex, data, descriptors, handler);
+              }
+            };
       }
 
       assert appView.verifyMovedMethodsHaveOriginalMethodPosition();
-
       // Generate the resulting application resources.
       writeApplication(appView, inputApp, executorService);
 
+      if (options.androidResourceProvider != null && options.androidResourceConsumer != null) {
+        shrinkResources(dexFileContent);
+      }
       assert appView.getDontWarnConfiguration().validate(options);
 
       options.printWarnings();
@@ -856,14 +878,68 @@
     }
   }
 
-  private void writeAndroidResources(
-      AndroidResourceProvider androidResourceProvider,
-      AndroidResourceConsumer androidResourceConsumer,
-      DiagnosticsHandler diagnosticsHandler) {
-    ResourceTracing resourceTracing = ResourceTracing.getImpl();
-    resourceTracing.setConsumer(androidResourceConsumer);
-    resourceTracing.setProvider(androidResourceProvider);
-    resourceTracing.done(diagnosticsHandler);
+  private void shrinkResources(List<Pair<Integer, byte[]>> dexFileContent) {
+    LegacyResourceShrinker.Builder resourceShrinkerBuilder = LegacyResourceShrinker.builder();
+    Reporter reporter = options.reporter;
+    dexFileContent.forEach(p -> resourceShrinkerBuilder.addDexInput(p.getFirst(), p.getSecond()));
+    try {
+      Collection<AndroidResourceInput> androidResources =
+          options.androidResourceProvider.getAndroidResources();
+      for (AndroidResourceInput androidResource : androidResources) {
+        try {
+          byte[] bytes = androidResource.getByteStream().readAllBytes();
+          Path path = Paths.get(androidResource.getPath().location());
+          switch (androidResource.getKind()) {
+            case MANIFEST:
+              resourceShrinkerBuilder.setManifest(path, bytes);
+              break;
+            case RES_FOLDER_FILE:
+              resourceShrinkerBuilder.addResFolderInput(path, bytes);
+              break;
+            case RESOURCE_TABLE:
+              resourceShrinkerBuilder.setResourceTable(path, bytes);
+              break;
+            case XML_FILE:
+              resourceShrinkerBuilder.addXmlInput(path, bytes);
+              break;
+            case UNKNOWN:
+              break;
+          }
+        } catch (IOException e) {
+          reporter.error(new ExceptionDiagnostic(e, androidResource.getOrigin()));
+        }
+      }
+
+      LegacyResourceShrinker shrinker = resourceShrinkerBuilder.build();
+      ShrinkerResult shrinkerResult = shrinker.run();
+      AndroidResourceConsumer androidResourceConsumer = options.androidResourceConsumer;
+      Set<String> toKeep = shrinkerResult.getResFolderEntriesToKeep();
+      for (AndroidResourceInput androidResource : androidResources) {
+        switch (androidResource.getKind()) {
+          case MANIFEST:
+          case UNKNOWN:
+            androidResourceConsumer.accept(
+                new R8PassThroughAndroidResource(androidResource, reporter), reporter);
+            break;
+          case RESOURCE_TABLE:
+            androidResourceConsumer.accept(
+                new R8AndroidResourceWithData(
+                    androidResource, reporter, shrinkerResult.getResourceTableInProtoFormat()),
+                reporter);
+            break;
+          case RES_FOLDER_FILE:
+          case XML_FILE:
+            if (toKeep.contains(androidResource.getPath().location())) {
+              androidResourceConsumer.accept(
+                  new R8PassThroughAndroidResource(androidResource, reporter), reporter);
+            }
+            break;
+        }
+      }
+      androidResourceConsumer.finished(reporter);
+    } catch (ParserConfigurationException | SAXException | ResourceException | IOException e) {
+      reporter.error(new ExceptionDiagnostic(e));
+    }
   }
 
   private static boolean allReferencesAssignedApiLevel(
@@ -1093,4 +1169,58 @@
     }
     ExceptionUtils.withMainProgramHandler(() -> run(args));
   }
+
+  private abstract static class R8AndroidResourceBase implements AndroidResourceOutput {
+
+    protected final AndroidResourceInput androidResource;
+    protected final Reporter reporter;
+
+    public R8AndroidResourceBase(AndroidResourceInput androidResource, Reporter reporter) {
+      this.androidResource = androidResource;
+      this.reporter = reporter;
+    }
+
+    @Override
+    public ResourcePath getPath() {
+      return androidResource.getPath();
+    }
+
+    @Override
+    public Origin getOrigin() {
+      return androidResource.getOrigin();
+    }
+  }
+
+  private static class R8PassThroughAndroidResource extends R8AndroidResourceBase {
+
+    public R8PassThroughAndroidResource(AndroidResourceInput androidResource, Reporter reporter) {
+      super(androidResource, reporter);
+    }
+
+    @Override
+    public ByteDataView getByteDataView() {
+      try {
+        return ByteDataView.of(ByteStreams.toByteArray(androidResource.getByteStream()));
+      } catch (IOException | ResourceException e) {
+        reporter.error(new ExceptionDiagnostic(e, androidResource.getOrigin()));
+      }
+      return null;
+    }
+  }
+
+  private static class R8AndroidResourceWithData extends R8AndroidResourceBase {
+
+    private final byte[] data;
+
+    public R8AndroidResourceWithData(
+        AndroidResourceInput androidResource, Reporter reporter, byte[] data) {
+      super(androidResource, reporter);
+      this.data = data;
+    }
+
+    @Override
+    public ByteDataView getByteDataView() {
+      return ByteDataView.of(data);
+    }
+  }
 }
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinkerImpl.kt b/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinkerImpl.kt
index 1136085..153f1ce 100644
--- a/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinkerImpl.kt
+++ b/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinkerImpl.kt
@@ -301,6 +301,11 @@
         .any { it.isReachable }
 }
 
+fun ResourceStore.isJarPathReachable(path: String) : Boolean {
+    val (_, folder, name) = path.split('/', limit = 3)
+    return isJarPathReachable(folder, name);
+}
+
 private fun ResourceStore.getResourceId(
     folder: String,
     name: String
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/graph/ProtoResourcesGraphBuilder.kt b/src/resourceshrinker/java/com/android/build/shrinker/graph/ProtoResourcesGraphBuilder.kt
index 130e585..a0a1f76 100644
--- a/src/resourceshrinker/java/com/android/build/shrinker/graph/ProtoResourcesGraphBuilder.kt
+++ b/src/resourceshrinker/java/com/android/build/shrinker/graph/ProtoResourcesGraphBuilder.kt
@@ -21,6 +21,7 @@
 import com.android.aapt.Resources.FileReference
 import com.android.aapt.Resources.FileReference.Type.PROTO_XML
 import com.android.aapt.Resources.Reference
+import com.android.aapt.Resources.ResourceTable
 import com.android.aapt.Resources.XmlAttribute
 import com.android.aapt.Resources.XmlElement
 import com.android.aapt.Resources.XmlNode
@@ -56,12 +57,23 @@
  * @param resourceTable path to resource table in proto format.
  */
 class ProtoResourcesGraphBuilder(
-    private val resourceRoot: Path,
-    private val resourceTable: Path
+    private val resourceRoot: ResFolderFileTree,
+    private val resourceTableProducer: (ResourceShrinkerModel) -> ResourceTable
 ) : ResourcesGraphBuilder {
 
+    constructor(resourceRootPath: Path, resourceTablePath: Path) : this(
+        object : ResFolderFileTree {
+            override fun getEntryByName(pathInRes: String): ByteArray {
+                val lazyVal : ByteArray by lazy {
+                    Files.readAllBytes(resourceRootPath.resolve(pathInRes))
+                }
+                return lazyVal
+            }
+        },
+        { model -> model.readResourceTable(resourceTablePath) }
+    )
     override fun buildGraph(model: ResourceShrinkerModel) {
-        model.readResourceTable(resourceTable).entriesSequence()
+        resourceTableProducer(model).entriesSequence()
             .map { (id, _, _, entry) ->
                 model.resourceStore.getResource(id)?.let {
                     ReferencesForResourceFinder(resourceRoot, model, entry, it)
@@ -71,9 +83,12 @@
             .forEach { it.findReferences() }
     }
 }
+interface ResFolderFileTree {
+    fun getEntryByName(pathInRes: String) : ByteArray
+}
 
 private class ReferencesForResourceFinder(
-    private val resourcesRoot: Path,
+    private val resourcesRoot: ResFolderFileTree,
     private val model: ResourceShrinkerModel,
     private val entry: Entry,
     private val current: Resource
@@ -196,10 +211,9 @@
     }
 
     private fun findFromFile(file: FileReference) {
-        val path = resourcesRoot.resolve(file.path.substringAfter("res/"))
-        val bytes: ByteArray by lazy { Files.readAllBytes(path) }
+        val bytes = resourcesRoot.getEntryByName(file.path.substringAfter("res/"))
         val content: String by lazy { String(bytes, StandardCharsets.UTF_8) }
-        val extension = Ascii.toLowerCase(path.fileName.toString()).substringAfter('.')
+        val extension = Ascii.toLowerCase(file.path.substringAfterLast('.'))
         when {
             file.type == PROTO_XML -> fillFromXmlNode(XmlNode.parseFrom(bytes))
             extension in listOf("html", "htm") -> webTokenizers.tokenizeHtml(content)
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;
+    }
+  }
+}
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 5f9bec6..d8ac6a9 100644
--- a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java
+++ b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java
@@ -32,7 +32,7 @@
     r8ResourceShrinkerModel.instantiateFromResourceTable(inputStream);
   }
 
-  private static class R8ResourceShrinkerModel extends ResourceShrinkerModel {
+  public static class R8ResourceShrinkerModel extends ResourceShrinkerModel {
 
     public R8ResourceShrinkerModel(
         ShrinkerDebugReporter debugReporter, boolean supportMultipackages) {
@@ -40,25 +40,29 @@
     }
 
     // Similar to instantiation in ProtoResourceTableGatherer, but using an inputstream.
-    public void instantiateFromResourceTable(InputStream inputStream) {
+    void instantiateFromResourceTable(InputStream inputStream) {
       try {
         ResourceTable resourceTable = ResourceTable.parseFrom(inputStream);
-        ResourceTableUtilKt.entriesSequence(resourceTable)
-            .iterator()
-            .forEachRemaining(
-                entryWrapper -> {
-                  ResourceType resourceType = ResourceType.fromClassName(entryWrapper.getType());
-                  if (resourceType != ResourceType.STYLEABLE) {
-                    this.addResource(
-                        resourceType,
-                        entryWrapper.getPackageName(),
-                        ResourcesUtil.resourceNameToFieldName(entryWrapper.getEntry().getName()),
-                        entryWrapper.getId());
-                  }
-                });
+        instantiateFromResourceTable(resourceTable);
       } catch (IOException ex) {
         throw new RuntimeException(ex);
       }
     }
+
+    void instantiateFromResourceTable(ResourceTable resourceTable) {
+      ResourceTableUtilKt.entriesSequence(resourceTable)
+          .iterator()
+          .forEachRemaining(
+              entryWrapper -> {
+                ResourceType resourceType = ResourceType.fromClassName(entryWrapper.getType());
+                if (resourceType != ResourceType.STYLEABLE) {
+                  this.addResource(
+                      resourceType,
+                      entryWrapper.getPackageName(),
+                      ResourcesUtil.resourceNameToFieldName(entryWrapper.getEntry().getName()),
+                      entryWrapper.getId());
+                }
+              });
+    }
   }
 }
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/usages/DexUsageRecorder.kt b/src/resourceshrinker/java/com/android/build/shrinker/usages/DexUsageRecorder.kt
index 3f1a676..5d6a0df 100644
--- a/src/resourceshrinker/java/com/android/build/shrinker/usages/DexUsageRecorder.kt
+++ b/src/resourceshrinker/java/com/android/build/shrinker/usages/DexUsageRecorder.kt
@@ -56,7 +56,7 @@
     }
 }
 
-private class DexFileAnalysisCallback(
+class DexFileAnalysisCallback(
         private val path: Path,
         private val model: ResourceShrinkerModel
 ) : AnalysisCallback {
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/usages/ProtoAndroidManifestUsageRecorder.kt b/src/resourceshrinker/java/com/android/build/shrinker/usages/ProtoAndroidManifestUsageRecorder.kt
index 4e6aa3e..77bb8f6 100644
--- a/src/resourceshrinker/java/com/android/build/shrinker/usages/ProtoAndroidManifestUsageRecorder.kt
+++ b/src/resourceshrinker/java/com/android/build/shrinker/usages/ProtoAndroidManifestUsageRecorder.kt
@@ -34,25 +34,25 @@
         recordUsagesFromNode(root, model)
     }
 
-    private fun recordUsagesFromNode(node: XmlNode, model: ResourceShrinkerModel) {
-        // Records only resources from element attributes that have reference items with resolved
-        // ids or names.
-        if (!node.hasElement()) {
-            return
-        }
-        node.element.attributeList.asSequence()
-            .filter { it.hasCompiledItem() }
-            .map { it.compiledItem }
-            .filter { it.hasRef() }
-            .map { it.ref }
-            .flatMap {
-                // If resource id is available prefer this id to name.
-                when {
-                    it.id != 0 -> listOfNotNull(model.resourceStore.getResource(it.id))
-                    else -> model.resourceStore.getResourcesFromUrl("@${it.name}")
-                }.asSequence()
-            }
-            .forEach { ResourceUsageModel.markReachable(it) }
-        node.element.childList.forEach { recordUsagesFromNode(it, model) }
+}
+fun recordUsagesFromNode(node: XmlNode, model: ResourceShrinkerModel) {
+    // Records only resources from element attributes that have reference items with resolved
+    // ids or names.
+    if (!node.hasElement()) {
+        return
     }
+    node.element.attributeList.asSequence()
+        .filter { it.hasCompiledItem() }
+        .map { it.compiledItem }
+        .filter { it.hasRef() }
+        .map { it.ref }
+        .flatMap {
+            // If resource id is available prefer this id to name.
+            when {
+                it.id != 0 -> listOfNotNull(model.resourceStore.getResource(it.id))
+                else -> model.resourceStore.getResourcesFromUrl("@${it.name}")
+            }.asSequence()
+        }
+        .forEach { ResourceUsageModel.markReachable(it) }
+    node.element.childList.forEach { recordUsagesFromNode(it, model) }
 }
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/usages/ToolsAttributeUsageRecorder.kt b/src/resourceshrinker/java/com/android/build/shrinker/usages/ToolsAttributeUsageRecorder.kt
index 0dae39a..8e43cd8 100644
--- a/src/resourceshrinker/java/com/android/build/shrinker/usages/ToolsAttributeUsageRecorder.kt
+++ b/src/resourceshrinker/java/com/android/build/shrinker/usages/ToolsAttributeUsageRecorder.kt
@@ -16,9 +16,11 @@
 
 package com.android.build.shrinker.usages
 
+import com.android.SdkConstants.TOOLS_NS_NAME
 import com.android.SdkConstants.VALUE_STRICT
 import com.android.build.shrinker.ResourceShrinkerModel
 import com.android.utils.XmlUtils
+import com.google.common.collect.ImmutableMap
 import com.google.common.collect.ImmutableMap.copyOf
 import java.io.Reader
 import java.nio.file.Files
@@ -37,9 +39,6 @@
  * @param rawResourcesPath path to folder with resources in raw format.
  */
 class ToolsAttributeUsageRecorder(val rawResourcesPath: Path) : ResourceUsageRecorder {
-    companion object {
-        private val TOOLS_NAMESPACE = "http://schemas.android.com/tools"
-    }
 
     override fun recordUsages(model: ResourceShrinkerModel) {
         Files.walk(rawResourcesPath)
@@ -48,42 +47,48 @@
     }
 
     private fun processRawXml(path: Path, model: ResourceShrinkerModel) {
-        processResourceToolsAttributes(path).forEach { key, value ->
-            when (key) {
-                "keep" -> model.resourceStore.recordKeepToolAttribute(value)
-                "discard" -> model.resourceStore.recordDiscardToolAttribute(value)
-                "shrinkMode" ->
-                    if (value == VALUE_STRICT) {
-                        model.resourceStore.safeMode = false
-                    }
-            }
-        }
-    }
-
-    private fun processResourceToolsAttributes(path: Path): Map<String, String> {
-        val toolsAttributes = mutableMapOf<String, String>()
-        XmlUtils.getUtfReader(path.toFile()).use { reader: Reader ->
-            val factory = XMLInputFactory.newInstance()
-            val xmlStreamReader = factory.createXMLStreamReader(reader)
-
-            var rootElementProcessed = false
-            while (!rootElementProcessed && xmlStreamReader.hasNext()) {
-                xmlStreamReader.next()
-                if (xmlStreamReader.isStartElement) {
-                    if (xmlStreamReader.localName == "resources") {
-                        for (i in 0 until xmlStreamReader.attributeCount) {
-                            if (xmlStreamReader.getAttributeNamespace(i) == TOOLS_NAMESPACE) {
-                                toolsAttributes.put(
-                                    xmlStreamReader.getAttributeLocalName(i),
-                                    xmlStreamReader.getAttributeValue(i)
-                                )
-                            }
-                        }
-                    }
-                    rootElementProcessed = true
-                }
-            }
-        }
-        return copyOf(toolsAttributes)
+        processRawXml(XmlUtils.getUtfReader(path.toFile()), model)
     }
 }
+
+fun processRawXml(reader: Reader, model: ResourceShrinkerModel) {
+    processResourceToolsAttributes(reader).forEach { key, value ->
+        when (key) {
+            "keep" -> model.resourceStore.recordKeepToolAttribute(value)
+            "discard" -> model.resourceStore.recordDiscardToolAttribute(value)
+            "shrinkMode" ->
+                if (value == VALUE_STRICT) {
+                    model.resourceStore.safeMode = false
+                }
+        }
+    }
+}
+
+fun processResourceToolsAttributes(utfReader: Reader?): ImmutableMap<String, String> {
+    val toolsAttributes = mutableMapOf<String, String>()
+    utfReader.use { reader: Reader? ->
+        val factory = XMLInputFactory.newInstance()
+        val xmlStreamReader = factory.createXMLStreamReader(reader)
+
+        var rootElementProcessed = false
+        while (!rootElementProcessed && xmlStreamReader.hasNext()) {
+            xmlStreamReader.next()
+            if (xmlStreamReader.isStartElement) {
+                if (xmlStreamReader.localName == "resources") {
+                    for (i in 0 until xmlStreamReader.attributeCount) {
+                        val namespace = "http://schemas.android.com/tools"
+                        if (xmlStreamReader.getAttributeNamespace(i) == namespace) {
+                            toolsAttributes.put(
+                                xmlStreamReader.getAttributeLocalName(i),
+                                xmlStreamReader.getAttributeValue(i)
+                            )
+                        }
+                    }
+                }
+                rootElementProcessed = true
+            }
+        }
+    }
+    return copyOf(toolsAttributes)
+}
+
diff --git a/src/test/java/com/android/tools/r8/R8TestBuilder.java b/src/test/java/com/android/tools/r8/R8TestBuilder.java
index 238594e..d55e5c5 100644
--- a/src/test/java/com/android/tools/r8/R8TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/R8TestBuilder.java
@@ -9,6 +9,7 @@
 
 import com.android.tools.r8.R8Command.Builder;
 import com.android.tools.r8.TestBase.Backend;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResource;
 import com.android.tools.r8.benchmarks.BenchmarkResults;
 import com.android.tools.r8.dexsplitter.SplitterTestBase.RunInterface;
 import com.android.tools.r8.dexsplitter.SplitterTestBase.SplitRunner;
@@ -16,6 +17,7 @@
 import com.android.tools.r8.experimental.graphinfo.GraphConsumer;
 import com.android.tools.r8.keepanno.KeepEdgeAnnotationsTest;
 import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.origin.PathOrigin;
 import com.android.tools.r8.profile.art.ArtProfileConsumer;
 import com.android.tools.r8.profile.art.ArtProfileProvider;
 import com.android.tools.r8.profile.art.model.ExternalArtProfile;
@@ -81,6 +83,7 @@
   private final List<Path> mainDexRulesFiles = new ArrayList<>();
   private final List<String> applyMappingMaps = new ArrayList<>();
   private final List<Path> features = new ArrayList<>();
+  private Path resourceShrinkerOutput = null;
   private PartitionMapConsumer partitionMapConsumer = null;
 
   @Override
@@ -141,6 +144,7 @@
     }
 
     class Box {
+
       private List<ProguardConfigurationRule> syntheticProguardRules;
       private ProguardConfiguration proguardConfiguration;
     }
@@ -166,7 +170,8 @@
             graphConsumer,
             getMinApiLevel(),
             features,
-            residualArtProfiles);
+            residualArtProfiles,
+            resourceShrinkerOutput);
     switch (allowedDiagnosticMessages) {
       case ALL:
         compileResult.getDiagnosticMessages().assertAllDiagnosticsMatch(new IsAnything<>());
@@ -868,4 +873,25 @@
     this.partitionMapConsumer = partitionMapConsumer;
     return self();
   }
+
+  public T addAndroidResources(AndroidTestResource testResource) throws IOException {
+    return addAndroidResources(
+        testResource, getState().getNewTempFile("resourceshrinkeroutput.zip"));
+  }
+
+  public T addAndroidResources(AndroidTestResource testResource, Path output) throws IOException {
+    addResourceShrinkerProviderAndConsumer(testResource.getResourceZip(), output);
+    return addProgramClassFileData(testResource.getRClass().getClassFileData());
+  }
+
+  private T addResourceShrinkerProviderAndConsumer(Path resources, Path output) throws IOException {
+    resourceShrinkerOutput = output;
+    getBuilder()
+        .setAndroidResourceProvider(
+            new ArchiveProtoAndroidResourceProvider(resources, new PathOrigin(resources)));
+    getBuilder()
+        .setAndroidResourceConsumer(
+            new ArchiveProtoAndroidResourceConsumer(resourceShrinkerOutput));
+    return self();
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/R8TestCompileResult.java b/src/test/java/com/android/tools/r8/R8TestCompileResult.java
index 1ca26f0..82997e9 100644
--- a/src/test/java/com/android/tools/r8/R8TestCompileResult.java
+++ b/src/test/java/com/android/tools/r8/R8TestCompileResult.java
@@ -6,8 +6,10 @@
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
 
 import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.ResourceTableInspector;
 import com.android.tools.r8.dexsplitter.SplitterTestBase.SplitRunner;
 import com.android.tools.r8.profile.art.model.ExternalArtProfile;
 import com.android.tools.r8.profile.art.utils.ArtProfileInspector;
@@ -19,6 +21,7 @@
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ThrowingBiConsumer;
 import com.android.tools.r8.utils.ThrowingConsumer;
+import com.android.tools.r8.utils.ZipUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.graphinspector.GraphInspector;
@@ -36,6 +39,7 @@
   private final CollectingGraphConsumer graphConsumer;
   private final List<Path> features;
   private final List<ExternalArtProfile> residualArtProfiles;
+  private final Path resourceShrinkerOutput;
 
   R8TestCompileResult(
       TestState state,
@@ -48,7 +52,8 @@
       CollectingGraphConsumer graphConsumer,
       int minApiLevel,
       List<Path> features,
-      List<ExternalArtProfile> residualArtProfiles) {
+      List<ExternalArtProfile> residualArtProfiles,
+      Path resourceShrinkerOutput) {
     super(state, app, minApiLevel, outputMode, libraryDesugaringTestConfiguration);
     this.proguardConfiguration = proguardConfiguration;
     this.syntheticProguardRules = syntheticProguardRules;
@@ -56,6 +61,7 @@
     this.graphConsumer = graphConsumer;
     this.features = features;
     this.residualArtProfiles = residualArtProfiles;
+    this.resourceShrinkerOutput = resourceShrinkerOutput;
   }
 
   @Override
@@ -149,6 +155,15 @@
     return self();
   }
 
+  public <E extends Throwable> R8TestCompileResult inspectShrunkenResources(
+      Consumer<ResourceTableInspector> consumer) throws IOException {
+    assertNotNull(resourceShrinkerOutput);
+    consumer.accept(
+        new ResourceTableInspector(
+            ZipUtils.readSingleEntry(resourceShrinkerOutput, "resources.pb")));
+    return self();
+  }
+
   public GraphInspector graphInspector() throws IOException {
     assert graphConsumer != null;
     return new GraphInspector(graphConsumer, inspector());
diff --git a/src/test/java/com/android/tools/r8/TestBuilder.java b/src/test/java/com/android/tools/r8/TestBuilder.java
index 4e98e36..84ccfd9 100644
--- a/src/test/java/com/android/tools/r8/TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestBuilder.java
@@ -136,10 +136,6 @@
     return addProgramClasses(classes).addInnerClasses(classes);
   }
 
-  public T addAndroidResources(AndroidTestResource testResource) throws IOException {
-    return addProgramClassFileData(testResource.getRClass().getClassFileData());
-  }
-
   public T addInnerClasses(Class<?>... classes) throws IOException {
     return addInnerClasses(Arrays.asList(classes));
   }
diff --git a/src/test/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java b/src/test/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java
index c1dde31..30960ee 100644
--- a/src/test/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java
+++ b/src/test/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java
@@ -6,6 +6,10 @@
 import static com.android.tools.r8.TestBase.javac;
 import static com.android.tools.r8.TestBase.transformer;
 
+import com.android.aapt.Resources;
+import com.android.aapt.Resources.ConfigValue;
+import com.android.aapt.Resources.Item;
+import com.android.aapt.Resources.ResourceTable;
 import com.android.tools.r8.TestRuntime.CfRuntime;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.ProcessResult;
@@ -16,16 +20,20 @@
 import com.android.tools.r8.utils.StreamUtils;
 import com.android.tools.r8.utils.ZipUtils;
 import com.google.common.collect.MoreCollectors;
+import com.google.protobuf.InvalidProtocolBufferException;
+import java.io.File;
 import java.io.IOException;
 import java.lang.reflect.Field;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.TreeMap;
 import java.util.stream.Collectors;
+import org.junit.Assert;
 import org.junit.rules.TemporaryFolder;
 
 public class AndroidResourceTestingUtils {
@@ -103,6 +111,92 @@
     }
   }
 
+  // Easy traversable resource table.
+  public static class TestResourceTable {
+    private Map<String, ResourceNameToValueMapping> mapping = new HashMap<>();
+
+    private TestResourceTable(ResourceTable resourceTable) {
+      // For now, we don't have any test that use multiple packages.
+      assert resourceTable.getPackageCount() == 1;
+      for (Resources.Type type : resourceTable.getPackage(0).getTypeList()) {
+        String typeName = type.getName();
+        mapping.put(typeName, new ResourceNameToValueMapping(type));
+      }
+    }
+
+    public static TestResourceTable parseFrom(byte[] bytes) throws InvalidProtocolBufferException {
+      return new TestResourceTable(ResourceTable.parseFrom(bytes));
+    }
+
+    public boolean containsValueFor(String type, String name) {
+      return mapping.containsKey(type) && mapping.get(type).containsValueFor(name);
+    }
+
+    public static class ResourceNameToValueMapping {
+      private final Map<String, List<ResourceValue>> mapping = new HashMap<>();
+
+      public ResourceNameToValueMapping(Resources.Type type) {
+        for (Resources.Entry entry : type.getEntryList()) {
+          String name = entry.getName();
+          List<ResourceValue> entries = new ArrayList<>();
+          for (ConfigValue configValue : entry.getConfigValueList()) {
+            Item item = configValue.getValue().getItem();
+            // Currently supporting files and strings, we just flatten this to strings for easy
+            // testing.
+            if (item.hasFile()) {
+              entries.add(
+                  new ResourceValue(item.getFile().getPath(), configValue.getConfig().toString()));
+            } else if (item.hasStr()) {
+              entries.add(
+                  new ResourceValue(item.getStr().getValue(), configValue.getConfig().toString()));
+            }
+            mapping.put(name, entries);
+          }
+        }
+      }
+
+      public boolean containsValueFor(String name) {
+        return mapping.containsKey(name);
+      }
+
+      public static class ResourceValue {
+
+        private final String value;
+        private final String config;
+
+        public ResourceValue(String value, String config) {
+          this.value = value;
+          this.config = config;
+        }
+
+        public String getValue() {
+          return value;
+        }
+
+        public String getConfig() {
+          return config;
+        }
+      }
+    }
+  }
+
+  public static class ResourceTableInspector {
+
+    private final TestResourceTable testResourceTable;
+
+    public ResourceTableInspector(byte[] bytes) throws InvalidProtocolBufferException {
+      testResourceTable = TestResourceTable.parseFrom(bytes);
+    }
+
+    public void assertContainsResourceWithName(String type, String name) {
+      Assert.assertTrue(testResourceTable.containsValueFor(type, name));
+    }
+
+    public void assertDoesNotContainResourceWithName(String type, String name) {
+      Assert.assertFalse(testResourceTable.containsValueFor(type, name));
+    }
+  }
+
   public static class AndroidTestResourceBuilder {
     private String manifest;
     private final Map<String, String> stringValues = new TreeMap<>();
@@ -160,13 +254,13 @@
         FileUtils.writeTextFile(
             temp.newFolder("res", "values").toPath().resolve("strings.xml"),
             createStringResourceXml());
+
       }
       if (drawables.size() > 0) {
+        File drawableFolder = temp.newFolder("res", "drawable");
         for (Entry<String, byte[]> entry : drawables.entrySet()) {
           FileUtils.writeToFile(
-              temp.newFolder("res", "drawable").toPath().resolve(entry.getKey()),
-              null,
-              entry.getValue());
+              drawableFolder.toPath().resolve(entry.getKey()), null, entry.getValue());
         }
       }
 
diff --git a/src/test/java/com/android/tools/r8/androidresources/AndroidResourcesPassthroughTest.java b/src/test/java/com/android/tools/r8/androidresources/AndroidResourcesPassthroughTest.java
deleted file mode 100644
index 41754aa..0000000
--- a/src/test/java/com/android/tools/r8/androidresources/AndroidResourcesPassthroughTest.java
+++ /dev/null
@@ -1,85 +0,0 @@
-// 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.tools.r8.androidresources;
-
-import static org.hamcrest.CoreMatchers.containsString;
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertThat;
-
-import com.android.tools.r8.ArchiveProtoAndroidResourceConsumer;
-import com.android.tools.r8.ArchiveProtoAndroidResourceProvider;
-import com.android.tools.r8.TestBase;
-import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResource;
-import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResourceBuilder;
-import com.android.tools.r8.origin.PathOrigin;
-import com.android.tools.r8.utils.FileUtils;
-import com.android.tools.r8.utils.ZipUtils;
-import java.nio.charset.Charset;
-import java.nio.file.Path;
-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;
-
-@RunWith(Parameterized.class)
-public class AndroidResourcesPassthroughTest extends TestBase {
-
-  @Parameter(0)
-  public TestParameters parameters;
-
-  @Parameters(name = "{0}")
-  public static TestParametersCollection parameters() {
-    return getTestParameters().withDexRuntimes().withAllApiLevels().build();
-  }
-
-  @Test
-  public void testR8() throws Exception {
-    String manifestPath = "AndroidManifest.xml";
-    String resourcePath = "resources.pb";
-    String pngPath = "res/drawable/foo.png";
-
-    AndroidTestResource testResource =
-        new AndroidTestResourceBuilder()
-            .withSimpleManifestAndAppNameString()
-            .addDrawable("foo.png", AndroidResourceTestingUtils.TINY_PNG)
-            .build(temp);
-    Path resources = testResource.getResourceZip();
-    Path output = temp.newFile("resources_out.zip").toPath();
-    testForR8(parameters.getBackend())
-        .addInnerClasses(getClass())
-        .setMinApi(parameters)
-        .addOptionsModification(
-            o -> {
-              o.androidResourceProvider =
-                  new ArchiveProtoAndroidResourceProvider(resources, new PathOrigin(resources));
-              o.androidResourceConsumer = new ArchiveProtoAndroidResourceConsumer(output);
-            })
-        .addKeepMainRule(FooBar.class)
-        .run(parameters.getRuntime(), FooBar.class)
-        .assertSuccessWithOutputLines("Hello World");
-    assertArrayEquals(
-        ZipUtils.readSingleEntry(output, manifestPath),
-        ZipUtils.readSingleEntry(resources, manifestPath));
-    assertArrayEquals(
-        ZipUtils.readSingleEntry(output, resourcePath),
-        ZipUtils.readSingleEntry(resources, resourcePath));
-    assertArrayEquals(
-        ZipUtils.readSingleEntry(output, pngPath), ZipUtils.readSingleEntry(resources, pngPath));
-    String rClassContent =
-        FileUtils.readTextFile(
-            testResource.getRClass().getJavaFilePath(), Charset.defaultCharset());
-    assertThat(rClassContent, containsString("app_name"));
-    assertThat(rClassContent, containsString("foo"));
-  }
-
-  public static class FooBar {
-
-    public static void main(String[] args) {
-      System.out.println("Hello World");
-    }
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/androidresources/SimpleNoCodeReferenceAndroidResourceTest.java b/src/test/java/com/android/tools/r8/androidresources/SimpleNoCodeReferenceAndroidResourceTest.java
new file mode 100644
index 0000000..34b2b9a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/androidresources/SimpleNoCodeReferenceAndroidResourceTest.java
@@ -0,0 +1,106 @@
+// 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.tools.r8.androidresources;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResource;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResourceBuilder;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.ResourceTableInspector;
+import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.ZipUtils;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.file.Path;
+import java.util.Arrays;
+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;
+
+@RunWith(Parameterized.class)
+public class SimpleNoCodeReferenceAndroidResourceTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection parameters() {
+    return getTestParameters().withDexRuntimes().withAllApiLevels().build();
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    String manifestPath = "AndroidManifest.xml";
+    String resourcePath = "resources.pb";
+    String pngPath = "res/drawable/foo.png";
+
+    AndroidTestResource testResource =
+        new AndroidTestResourceBuilder()
+            .withSimpleManifestAndAppNameString()
+            .addDrawable("foo.png", AndroidResourceTestingUtils.TINY_PNG)
+            .addDrawable("bar.png", AndroidResourceTestingUtils.TINY_PNG)
+            .build(temp);
+    Path resources = testResource.getResourceZip();
+    Path output = temp.newFile("resources_out.zip").toPath();
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .setMinApi(parameters)
+        .addAndroidResources(testResource, output)
+        .addKeepMainRule(FooBar.class)
+        .compile()
+        .inspectShrunkenResources(
+            shrunkenInspector -> {
+              // Reachable from the manifest
+              shrunkenInspector.assertContainsResourceWithName("string", "app_name");
+              // Not reachable from anything
+              shrunkenInspector.assertDoesNotContainResourceWithName("drawable", "foo");
+              shrunkenInspector.assertDoesNotContainResourceWithName("drawable", "bar");
+              try {
+                assertFalse(ZipUtils.containsEntry(output, pngPath));
+              } catch (IOException e) {
+                throw new RuntimeException(e);
+              }
+            })
+        .run(parameters.getRuntime(), FooBar.class)
+        .assertSuccessWithOutputLines("Hello World");
+    // We don't touch the manifest
+    assertArrayEquals(
+        ZipUtils.readSingleEntry(output, manifestPath),
+        ZipUtils.readSingleEntry(resources, manifestPath));
+
+    String rClassContent =
+        FileUtils.readTextFile(
+            testResource.getRClass().getJavaFilePath(), Charset.defaultCharset());
+    assertFalse(
+        Arrays.equals(
+            ZipUtils.readSingleEntry(output, resourcePath),
+            ZipUtils.readSingleEntry(resources, resourcePath)));
+    assertThat(rClassContent, containsString("app_name"));
+    assertThat(rClassContent, containsString("foo"));
+    assertThat(rClassContent, containsString("bar"));
+    assertTrue(ZipUtils.containsEntry(resources, pngPath));
+    ResourceTableInspector resourceTableInspector =
+        new ResourceTableInspector(
+            ZipUtils.readSingleEntry(testResource.getResourceZip(), resourcePath));
+    resourceTableInspector.assertContainsResourceWithName("string", "app_name");
+    resourceTableInspector.assertContainsResourceWithName("drawable", "foo");
+    resourceTableInspector.assertContainsResourceWithName("drawable", "bar");
+  }
+
+  public static class FooBar {
+
+    public static void main(String[] args) {
+      System.out.println("Hello World");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/androidresources/TestShrinkingWithCodeReferences.java b/src/test/java/com/android/tools/r8/androidresources/TestShrinkingWithCodeReferences.java
new file mode 100644
index 0000000..00b000f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/androidresources/TestShrinkingWithCodeReferences.java
@@ -0,0 +1,84 @@
+// 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.tools.r8.androidresources;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResource;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResourceBuilder;
+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 TestShrinkingWithCodeReferences extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection parameters() {
+    return getTestParameters().withDexRuntimes().withAllApiLevels().build();
+  }
+
+  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 {
+    testForR8(parameters.getBackend())
+        .setMinApi(parameters)
+        .addProgramClasses(FooBar.class)
+        .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();
+  }
+
+  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;
+    }
+  }
+}