Add resource shrinker sources to R8 repo

This is a verbatim copy for now since this will allow us to easily
merge in either direction - downside is that this does not pass our formatter.

I have added 8.2.20-dev to third_party/r8 to allow us to compile this standalone, this is not used when compiled into r8 (in a follow up). We should be able to refactor the setup to eventually not need this hack.

Add build setup in new gradle build files

Bug: 287398085
Change-Id: Ib9ab46acb67dc4d713e91b82f27b30ef67fb065a
diff --git a/d8_r8/resourceshrinker/build.gradle.kts b/d8_r8/resourceshrinker/build.gradle.kts
new file mode 100644
index 0000000..c15b7b8
--- /dev/null
+++ b/d8_r8/resourceshrinker/build.gradle.kts
@@ -0,0 +1,58 @@
+// 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.
+
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+plugins {
+  `kotlin-dsl`
+  id("dependencies-plugin")
+}
+
+java {
+  sourceSets.main.configure {
+    kotlin.srcDir(getRoot().resolveAll("src", "resourceshrinker", "java"))
+    java.srcDir(getRoot().resolveAll("src", "resourceshrinker", "java"))
+  }
+  sourceCompatibility = JvmCompatibility.sourceCompatibility
+  targetCompatibility = JvmCompatibility.targetCompatibility
+}
+
+fun jarDependencies() : FileCollection {
+  return sourceSets
+    .main
+    .get()
+    .compileClasspath
+    .filter({ "$it".contains("third_party")
+              && "$it".contains("dependencies")
+    })
+}
+
+tasks {
+  withType<KotlinCompile> {
+    kotlinOptions {
+      // We cannot use languageVersion.set(JavaLanguageVersion.of(8)) because gradle cannot figure
+      // out that the jdk is 1_8 and will try to download it.
+      jvmTarget = "11"
+    }
+  }
+
+  val depsJar by registering(Jar::class) {
+    println(header("Resource shrinker dependencies"))
+    jarDependencies().forEach({ println(it) })
+    from(jarDependencies().map(::zipTree))
+    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
+    archiveFileName.set("resourceshrinker_deps.jar")
+  }
+}
+
+dependencies {
+  compileOnly(Deps.asm)
+  compileOnly(Deps.guava)
+  compileOnly(files(getRoot().resolve("third_party/r8/r8lib_8.2.20-dev.jar")))
+  implementation("com.android.tools.build:aapt2-proto:8.2.0-alpha10-10154469")
+  implementation("com.google.protobuf:protobuf-java:3.19.3")
+  implementation("com.android.tools.layoutlib:layoutlib-api:31.2.0-alpha10")
+  implementation("com.android.tools:common:31.2.0-alpha10")
+  implementation("com.android.tools:sdk-common:31.2.0-alpha10")
+}
diff --git a/d8_r8/resourceshrinker/gradle.properties b/d8_r8/resourceshrinker/gradle.properties
new file mode 100644
index 0000000..1de43f9
--- /dev/null
+++ b/d8_r8/resourceshrinker/gradle.properties
@@ -0,0 +1,17 @@
+# 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.
+
+org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8
+kotlin.daemon.jvmargs=-Xmx3g -Dkotlin.js.compiler.legacy.force_enabled=true
+systemProp.file.encoding=UTF-8
+
+# Enable new incremental compilation
+kotlin.incremental.useClasspathSnapshot=true
+
+org.gradle.parallel=true
+org.gradle.caching=true
+
+# Do not download any jdks or detect them. We provide them.
+org.gradle.java.installations.auto-detect=false
+org.gradle.java.installations.auto-download=false
diff --git a/d8_r8/resourceshrinker/settings.gradle.kts b/d8_r8/resourceshrinker/settings.gradle.kts
new file mode 100644
index 0000000..604d9cb
--- /dev/null
+++ b/d8_r8/resourceshrinker/settings.gradle.kts
@@ -0,0 +1,27 @@
+// 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.
+
+pluginManagement {
+  repositories {
+    maven {
+      url = uri("file:../../third_party/dependencies")
+    }
+    maven {
+      url = uri("file:../../third_party/dependencies_new")
+    }
+  }
+}
+
+dependencyResolutionManagement {
+  repositories {
+    maven {
+      url = uri("file:../../third_party/dependencies")
+    }
+    maven {
+      url = uri("file:../../third_party/dependencies_new")
+    }
+  }
+}
+
+rootProject.name = "resourceshrinker"
diff --git a/d8_r8/settings.gradle.kts b/d8_r8/settings.gradle.kts
index 77ad2e0..a02c904 100644
--- a/d8_r8/settings.gradle.kts
+++ b/d8_r8/settings.gradle.kts
@@ -50,6 +50,7 @@
 // This project is temporarily located in d8_r8. When moved to root, the parent
 // folder should just be removed.
 includeBuild(root.resolve("keepanno"))
+includeBuild(root.resolve("resourceshrinker"))
 
 // We need to include src/main as a composite-build otherwise our test-modules
 // will compete with the test to compile the source files.
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/DummyContent.java b/src/resourceshrinker/java/com/android/build/shrinker/DummyContent.java
new file mode 100644
index 0000000..b9aaa3e
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/DummyContent.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.build.shrinker;
+
+public class DummyContent {
+
+  // A 1x1 pixel PNG of type BufferedImage.TYPE_BYTE_GRAY
+  public static final byte[] TINY_PNG =
+      new byte[] {
+        (byte) -119, (byte) 80, (byte) 78, (byte) 71, (byte) 13, (byte) 10,
+        (byte) 26, (byte) 10, (byte) 0, (byte) 0, (byte) 0, (byte) 13,
+        (byte) 73, (byte) 72, (byte) 68, (byte) 82, (byte) 0, (byte) 0,
+        (byte) 0, (byte) 1, (byte) 0, (byte) 0, (byte) 0, (byte) 1,
+        (byte) 8, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 58,
+        (byte) 126, (byte) -101, (byte) 85, (byte) 0, (byte) 0, (byte) 0,
+        (byte) 10, (byte) 73, (byte) 68, (byte) 65, (byte) 84, (byte) 120,
+        (byte) -38, (byte) 99, (byte) 96, (byte) 0, (byte) 0, (byte) 0,
+        (byte) 2, (byte) 0, (byte) 1, (byte) -27, (byte) 39, (byte) -34,
+        (byte) -4, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 73,
+        (byte) 69, (byte) 78, (byte) 68, (byte) -82, (byte) 66, (byte) 96,
+        (byte) -126
+      };
+
+  public static final long TINY_PNG_CRC = 0x88b2a3b0L;
+
+  // A 3x3 pixel PNG of type BufferedImage.TYPE_INT_ARGB with 9-patch markers
+  public static final byte[] TINY_9PNG =
+      new byte[] {
+        (byte) -119, (byte) 80, (byte) 78, (byte) 71, (byte) 13, (byte) 10,
+        (byte) 26, (byte) 10, (byte) 0, (byte) 0, (byte) 0, (byte) 13,
+        (byte) 73, (byte) 72, (byte) 68, (byte) 82, (byte) 0, (byte) 0,
+        (byte) 0, (byte) 3, (byte) 0, (byte) 0, (byte) 0, (byte) 3,
+        (byte) 8, (byte) 6, (byte) 0, (byte) 0, (byte) 0, (byte) 86,
+        (byte) 40, (byte) -75, (byte) -65, (byte) 0, (byte) 0, (byte) 0,
+        (byte) 20, (byte) 73, (byte) 68, (byte) 65, (byte) 84, (byte) 120,
+        (byte) -38, (byte) 99, (byte) 96, (byte) -128, (byte) -128, (byte) -1,
+        (byte) 12, (byte) 48, (byte) 6, (byte) 8, (byte) -96, (byte) 8,
+        (byte) -128, (byte) 8, (byte) 0, (byte) -107, (byte) -111, (byte) 7,
+        (byte) -7, (byte) -64, (byte) -82, (byte) 8, (byte) 0, (byte) 0,
+        (byte) 0, (byte) 0, (byte) 0, (byte) 73, (byte) 69, (byte) 78,
+        (byte) 68, (byte) -82, (byte) 66, (byte) 96, (byte) -126
+      };
+
+  public static final long TINY_9PNG_CRC = 0x1148f987L;
+
+  // The XML document <x/> as binary-packed with AAPT
+  public static final byte[] TINY_BINARY_XML =
+      new byte[] {
+        (byte) 3, (byte) 0, (byte) 8, (byte) 0, (byte) 104, (byte) 0,
+        (byte) 0, (byte) 0, (byte) 1, (byte) 0, (byte) 28, (byte) 0,
+        (byte) 36, (byte) 0, (byte) 0, (byte) 0, (byte) 1, (byte) 0,
+        (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0,
+        (byte) 0, (byte) 1, (byte) 0, (byte) 0, (byte) 32, (byte) 0,
+        (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0,
+        (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 1, (byte) 1,
+        (byte) 120, (byte) 0, (byte) 2, (byte) 1, (byte) 16, (byte) 0,
+        (byte) 36, (byte) 0, (byte) 0, (byte) 0, (byte) 1, (byte) 0,
+        (byte) 0, (byte) 0, (byte) -1, (byte) -1, (byte) -1, (byte) -1,
+        (byte) -1, (byte) -1, (byte) -1, (byte) -1, (byte) 0, (byte) 0,
+        (byte) 0, (byte) 0, (byte) 20, (byte) 0, (byte) 20, (byte) 0,
+        (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0,
+        (byte) 0, (byte) 0, (byte) 3, (byte) 1, (byte) 16, (byte) 0,
+        (byte) 24, (byte) 0, (byte) 0, (byte) 0, (byte) 1, (byte) 0,
+        (byte) 0, (byte) 0, (byte) -1, (byte) -1, (byte) -1, (byte) -1,
+        (byte) -1, (byte) -1, (byte) -1, (byte) -1, (byte) 0, (byte) 0,
+        (byte) 0, (byte) 0
+      };
+
+  public static final long TINY_BINARY_XML_CRC = 0xd7e65643L;
+
+  // The XML document <x/> as a proto packed with AAPT2
+  public static final byte[] TINY_PROTO_XML =
+      new byte[] {0xa, 0x3, 0x1a, 0x1, 0x78, 0x1a, 0x2, 0x8, 0x1};
+  public static final long TINY_PROTO_XML_CRC = 3204905971L;
+
+  // The XML document <x/> as binary-packed with AAPT
+  public static final byte[] TINY_PROTO_CONVERTED_TO_BINARY_XML =
+      new byte[] {
+        (byte) 3, (byte) 0, (byte) 8, (byte) 0, (byte) 112, (byte) 0,
+        (byte) 0, (byte) 0, (byte) 1, (byte) 0, (byte) 28, (byte) 0,
+        (byte) 36, (byte) 0, (byte) 0, (byte) 0, (byte) 1, (byte) 0,
+        (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0,
+        (byte) 0, (byte) 1, (byte) 0, (byte) 0, (byte) 32, (byte) 0,
+        (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0,
+        (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 1, (byte) 1,
+        (byte) 120, (byte) 0, (byte) -128, (byte) 1, (byte) 8, (byte) 0,
+        (byte) 8, (byte) 0, (byte) 0, (byte) 0, (byte) 2, (byte) 1,
+        (byte) 16, (byte) 0, (byte) 36, (byte) 0, (byte) 0, (byte) 0,
+        (byte) 1, (byte) 0, (byte) 0, (byte) 0, (byte) -1, (byte) -1,
+        (byte) -1, (byte) -1, (byte) -1, (byte) -1, (byte) -1, (byte) -1,
+        (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 20, (byte) 0,
+        (byte) 20, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0,
+        (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 3, (byte) 1,
+        (byte) 16, (byte) 0, (byte) 24, (byte) 0, (byte) 0, (byte) 0,
+        (byte) 1, (byte) 0, (byte) 0, (byte) 0, (byte) -1, (byte) -1,
+        (byte) -1, (byte) -1, (byte) -1, (byte) -1, (byte) -1, (byte) -1,
+        (byte) 0, (byte) 0, (byte) 0, (byte) 0
+      };
+
+  private DummyContent() {}
+}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/LinkedResourcesFormat.java b/src/resourceshrinker/java/com/android/build/shrinker/LinkedResourcesFormat.java
new file mode 100644
index 0000000..67b06a8
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/LinkedResourcesFormat.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.build.shrinker;
+
+public enum LinkedResourcesFormat {
+  BINARY,
+  PROTO,
+}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/PossibleResourcesMarker.java b/src/resourceshrinker/java/com/android/build/shrinker/PossibleResourcesMarker.java
new file mode 100644
index 0000000..5089061
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/PossibleResourcesMarker.java
@@ -0,0 +1,372 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.build.shrinker;
+
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static java.lang.Character.isDigit;
+
+import com.android.annotations.NonNull;
+import com.android.ide.common.resources.usage.ResourceStore;
+import com.android.ide.common.resources.usage.ResourceUsageModel;
+import com.android.ide.common.resources.usage.ResourceUsageModel.Resource;
+import com.android.resources.ResourceType;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Streams;
+import com.google.common.primitives.Ints;
+import java.util.List;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+import java.util.stream.Stream;
+
+/** Marks resources which are possibly referenced by string constants as reachable. */
+public class PossibleResourcesMarker {
+
+    // Copied from StringFormatDetector
+    // See java.util.Formatter docs
+    public static final Pattern FORMAT =
+            Pattern.compile(
+                    // Generic format:
+                    //   %[argument_index$][flags][width][.precision]conversion
+                    //
+                    "%"
+                            +
+                            // Argument Index
+                            "(\\d+\\$)?"
+                            +
+                            // Flags
+                            "([-+#, 0(<]*)?"
+                            +
+                            // Width
+                            "(\\d+)?"
+                            +
+                            // Precision
+                            "(\\.\\d+)?"
+                            +
+                            // Conversion. These are all a single character, except date/time
+                            // conversions
+                            // which take a prefix of t/T:
+                            "([tT])?"
+                            +
+                            // The current set of conversion characters are
+                            // b,h,s,c,d,o,x,e,f,g,a,t (as well as all those as upper-case
+                            // characters), plus
+                            // n for newlines and % as a literal %. And then there are all the
+                            // time/date
+                            // characters: HIKLm etc. Just match on all characters here since there
+                            // should
+                            // be at least one.
+                            "([a-zA-Z%])");
+
+    static final String NO_MATCH = "-nomatch-";
+
+    private final ShrinkerDebugReporter debugReporter;
+    private final ResourceStore resourceStore;
+    private final Set<String> strings;
+    private final boolean foundWebContent;
+
+    public PossibleResourcesMarker(ShrinkerDebugReporter debugReporter,
+                                   ResourceStore resourceStore,
+                                   Set<String> strings,
+                                   boolean foundWebContent) {
+        this.debugReporter = debugReporter;
+        this.resourceStore = resourceStore;
+        this.strings = strings;
+        this.foundWebContent = foundWebContent;
+    }
+
+    public void markPossibleResourcesReachable() {
+        Set<String> names =
+                resourceStore.getResources().stream()
+                        .map(resource -> resource.name)
+                        .collect(toImmutableSet());
+
+        int shortest = names.stream().mapToInt(String::length).min().orElse(Integer.MAX_VALUE);
+
+        // Check whether the string looks relevant
+        // We consider four types of strings:
+        //  (1) simple resource names, e.g. "foo" from @layout/foo
+        //      These might be the parameter to a getIdentifier() call, or could
+        //      be composed into a fully qualified resource name for the getIdentifier()
+        //      method. We match these for *all* resource types.
+        //  (2) Relative source names, e.g. layout/foo, from @layout/foo
+        //      These might be composed into a fully qualified resource name for
+        //      getIdentifier().
+        //  (3) Fully qualified resource names of the form package:type/name.
+        //  (4) If foundWebContent is true, look for android_res/ URL strings as well
+        strings.stream()
+                .filter(string -> string.length() >= shortest)
+                .flatMap(
+                        string -> {
+                            int n = string.length();
+                            boolean justName = true;
+                            boolean formatting = false;
+                            boolean haveSlash = false;
+                            for (int i = 0; i < n; i++) {
+                                char c = string.charAt(i);
+                                haveSlash |= c == '/';
+                                formatting |= c == '%';
+                                justName =
+                                        justName && !(c == '.' || c == ':' || c == '%' || c == '/');
+                            }
+
+                            Stream<Resource> reachable = Streams.concat(
+                                    foundWebContent
+                                            ? possibleWebResources(names, string)
+                                            : Stream.empty(),
+                                    justName ? possiblePrefixMatch(string) : Stream.empty(),
+                                    formatting && !haveSlash
+                                            ? possibleFormatting(string)
+                                            : Stream.empty(),
+                                    haveSlash
+                                            ? possibleTypedResource(names, string)
+                                            : Stream.empty(),
+                                    possibleIntResource(string));
+
+                            return reachable
+                                    .peek(resource -> debugReporter.debug(() -> "Marking "
+                                    + resource + " used because it matches string pool constant "
+                                    + string));
+                        })
+                .forEach(ResourceUsageModel::markReachable);
+    }
+
+    private Stream<Resource> possibleWebResources(
+            Set<String> names, String string) {
+        // Look for android_res/ URL strings.
+        List<Resource> resources = resourceStore.getResourcesFromWebUrl(string);
+        if (!resources.isEmpty()) {
+            return resources.stream();
+        }
+
+        int start = Math.max(string.lastIndexOf('/'), 0);
+        int dot = string.indexOf('.', start);
+        String name = string.substring(start, dot != -1 ? dot : string.length());
+
+        if (names.contains(name)) {
+            return resourceStore.getResourceMaps().stream()
+                    .filter(map -> map.containsKey(name))
+                    .flatMap(map -> map.get(name).stream());
+        }
+        return Stream.empty();
+    }
+
+    private Stream<Resource> possiblePrefixMatch(String string) {
+        // Check for a simple prefix match, e.g. as in
+        // getResources().getIdentifier("ic_video_codec_" + codecName, "drawable", ...)
+        return resourceStore.getResources().stream()
+                .filter(resource -> resource.name.startsWith(string));
+    }
+
+    private Stream<Resource> possibleFormatting(String string) {
+        // Possibly a formatting string, e.g.
+        //   String name = String.format("my_prefix_%1d", index);
+        //   int res = getContext().getResources().getIdentifier(name, "drawable", ...)
+        try {
+            Pattern pattern = Pattern.compile(convertFormatStringToRegexp(string));
+            return resourceStore.getResources().stream()
+                    .filter(resource -> pattern.matcher(resource.name).matches());
+        } catch (PatternSyntaxException ignored) {
+            return Stream.empty();
+        }
+    }
+
+    private Stream<Resource> possibleTypedResource(
+            Set<String> names, String string) {
+        // Try to pick out the resource name pieces; if we can find the
+        // resource type unambiguously; if not, just match on names
+        int slash = string.indexOf('/');
+        String name = string.substring(slash + 1);
+        if (name.isEmpty() || !names.contains(name)) {
+            return Stream.empty();
+        }
+        // See if have a known specific resource type
+        if (slash > 0) {
+            int colon = string.indexOf(':');
+            String typeName = string.substring(colon + 1, slash);
+            ResourceType type = ResourceType.fromClassName(typeName);
+            return type != null
+                    ? resourceStore.getResources(type, name).stream()
+                    : Stream.empty();
+        }
+        return resourceStore.getResourceMaps().stream()
+                .filter(map -> map.containsKey(name))
+                .flatMap(map -> map.get(name).stream());
+    }
+
+    private Stream<Resource> possibleIntResource(String string) {
+        // Just a number? There are cases where it calls getIdentifier by
+        // a String number; see for example SuggestionsAdapter in the support
+        // library which reports supporting a string like "2130837524" and
+        // "android.resource://com.android.alarmclock/2130837524".
+        String withoutSlash = string.substring(string.lastIndexOf('/') + 1);
+        if (withoutSlash.isEmpty() || !isDigit(withoutSlash.charAt(0))) {
+            return Stream.empty();
+        }
+        Integer id = Ints.tryParse(withoutSlash);
+        Resource resource = null;
+        if (id != null) {
+            resource = resourceStore.getResource(id);
+        }
+        return resource != null ? Stream.of(resource) : Stream.empty();
+    }
+
+    @VisibleForTesting
+    static String convertFormatStringToRegexp(String formatString) {
+        StringBuilder regexp = new StringBuilder();
+        int from = 0;
+        boolean hasEscapedLetters = false;
+        Matcher matcher = FORMAT.matcher(formatString);
+        int length = formatString.length();
+        while (matcher.find(from)) {
+            int start = matcher.start();
+            int end = matcher.end();
+            if (start == 0 && end == length) {
+                // Don't match if the entire string literal starts with % and ends with
+                // the a formatting character, such as just "%d": this just matches absolutely
+                // everything and is unlikely to be used in a resource lookup
+                return NO_MATCH;
+            }
+            if (start > from) {
+                hasEscapedLetters |= appendEscapedPattern(formatString, regexp, from, start);
+            }
+            String pattern = ".*";
+            String conversion = matcher.group(6);
+            String timePrefix = matcher.group(5);
+
+            //noinspection VariableNotUsedInsideIf,StatementWithEmptyBody: for readability.
+            if (timePrefix != null) {
+                // date notation; just use .* to match these
+            } else if (conversion != null && conversion.length() == 1) {
+                char type = conversion.charAt(0);
+                switch (type) {
+                    case 's':
+                    case 'S':
+                    case 't':
+                    case 'T':
+                        // Match everything
+                        break;
+                    case '%':
+                        pattern = "%";
+                        break;
+                    case 'n':
+                        pattern = "\n";
+                        break;
+                    case 'c':
+                    case 'C':
+                        pattern = ".";
+                        break;
+                    case 'x':
+                    case 'X':
+                        pattern = "\\p{XDigit}+";
+                        break;
+                    case 'd':
+                    case 'o':
+                        pattern = "\\p{Digit}+";
+                        break;
+                    case 'b':
+                        pattern = "(true|false)";
+                        break;
+                    case 'B':
+                        pattern = "(TRUE|FALSE)";
+                        break;
+                    case 'h':
+                    case 'H':
+                        pattern = "(null|\\p{XDigit}+)";
+                        break;
+                    case 'f':
+                        pattern = "-?[\\p{XDigit},.]+";
+                        break;
+                    case 'e':
+                        pattern = "-?\\p{Digit}+[,.]\\p{Digit}+e\\+?\\p{Digit}+";
+                        break;
+                    case 'E':
+                        pattern = "-?\\p{Digit}+[,.]\\p{Digit}+E\\+?\\p{Digit}+";
+                        break;
+                    case 'a':
+                        pattern = "0x[\\p{XDigit},.+p]+";
+                        break;
+                    case 'A':
+                        pattern = "0X[\\p{XDigit},.+P]+";
+                        break;
+                    case 'g':
+                    case 'G':
+                        pattern = "-?[\\p{XDigit},.+eE]+";
+                        break;
+                }
+
+                // Allow space or 0 prefix
+                if (!".*".equals(pattern)) {
+                    String width = matcher.group(3);
+                    //noinspection VariableNotUsedInsideIf
+                    if (width != null) {
+                        String flags = matcher.group(2);
+                        if ("0".equals(flags)) {
+                            pattern = "0*" + pattern;
+                        } else {
+                            pattern = " " + pattern;
+                        }
+                    }
+                }
+
+                // If it's a general .* wildcard which follows a previous .* wildcard,
+                // just skip it (e.g. don't convert %s%s into .*.*; .* is enough.)
+                int regexLength = regexp.length();
+                if (!".*".equals(pattern)
+                        || regexLength < 2
+                        || regexp.charAt(regexLength - 1) != '*'
+                        || regexp.charAt(regexLength - 2) != '.') {
+                    regexp.append(pattern);
+                }
+            }
+            from = end;
+        }
+
+        if (from < length) {
+            hasEscapedLetters |= appendEscapedPattern(formatString, regexp, from, length);
+        }
+
+        if (!hasEscapedLetters) {
+            // If the regexp contains *only* formatting characters, e.g. "%.0f%d", or
+            // if it contains only formatting characters and punctuation, e.g. "%s_%d",
+            // don't treat this as a possible resource name pattern string: it is unlikely
+            // to be intended for actual resource names, and has the side effect of matching
+            // most names.
+            return NO_MATCH;
+        }
+
+        return regexp.toString();
+    }
+
+    /**
+     * Appends the characters in the range [from,to> from formatString as escaped regexp characters
+     * into the given string builder. Returns true if there were any letters in the appended text.
+     */
+    private static boolean appendEscapedPattern(
+            @NonNull String formatString, @NonNull StringBuilder regexp, int from, int to) {
+        regexp.append(Pattern.quote(formatString.substring(from, to)));
+
+        for (int i = from; i < to; i++) {
+            if (Character.isLetter(formatString.charAt(i))) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinker.java b/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinker.java
new file mode 100644
index 0000000..e38056a
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinker.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.build.shrinker;
+
+import com.android.annotations.NonNull;
+import java.io.File;
+import java.io.IOException;
+import java.util.Map;
+import javax.xml.parsers.ParserConfigurationException;
+import org.xml.sax.SAXException;
+
+/**
+ * Interface for unit that analyzes all resources (after resource merging, compilation and code
+ * shrinking has been completed) and figures out which resources are unused, and replaces them with
+ * dummy content inside zip archive file.
+ */
+public interface ResourceShrinker extends AutoCloseable {
+
+    /**
+     * Analyzes resources and detects unreachable ones. It includes the following steps:
+     *
+     * <ul>
+     *     <li>Gather resources available in project.
+     *     <li>Record resource usages via analyzing compiled code, AndroidManifest etc.
+     *     <li>Build reference graph and connect dependent resources.
+     *     <li>Detects WebView and/or {@code Resources#getIdentifier} usages and guess which
+     *         resources are reachable analyzing string constants available in compiled code.
+     *     <li>Processes resources explicitly asked to keep and discard. &lt;tools:keep&gt; and
+     *         &lt;tools:discard&gt; attribute.
+     *     <li>Based on the root references computes unreachable resources.
+     * </ul>
+     */
+    void analyze() throws IOException, ParserConfigurationException, SAXException;
+
+    /**
+     * Returns count of unused resources. Should be called after {@code ResourceShrinker#analyze}.
+     */
+    int getUnusedResourceCount();
+
+    /**
+     * Replaces entries in {@param source} zip archive that belong to unused resources with dummy
+     * content and produces a new {@param dest} zip archive. Zip archive should contain resources
+     * in 'res/' folder like it is stored in APK.
+     *
+     * <p>For now, doesn't change resource table and applies to file-based resources like layouts,
+     * menus and drawables, not value-based resources like strings and dimensions.
+     *
+     * <p>Should be called after {@code ResourceShrinker#analyze}.
+     */
+    void rewriteResourcesInApkFormat(
+            @NonNull File source,
+            @NonNull File dest,
+            @NonNull LinkedResourcesFormat format
+    ) throws IOException;
+
+    /**
+     * Replaces entries in {@param source} zip archive that belong to unused resources with dummy
+     * content and produces a new {@param dest} zip archive. Zip archive represents App Bundle which
+     * may have multiple modules and each module has its own resource directory: '${module}/res/'.
+     * Package name for each bundle module should be passed as {@param moduleToPackageName}.
+     *
+     * <p>For now, doesn't change resource table and applies to file-based resources like layouts,
+     * menus and drawables, not value-based resources like strings and dimensions.
+     *
+     * <p>Should be called after {@code ResourceShrinker#analyze}.
+     */
+    void rewriteResourcesInBundleFormat(
+            @NonNull File source,
+            @NonNull File dest,
+            @NonNull Map<String, String> moduleToPackageName
+    ) throws IOException;
+}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinkerCli.java b/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinkerCli.java
new file mode 100644
index 0000000..3e30568
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinkerCli.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.build.shrinker;
+
+import com.android.build.shrinker.gatherer.ProtoResourceTableGatherer;
+import com.android.build.shrinker.gatherer.ResourcesGatherer;
+import com.android.build.shrinker.graph.ProtoResourcesGraphBuilder;
+import com.android.build.shrinker.usages.DexUsageRecorder;
+import com.android.build.shrinker.usages.ProtoAndroidManifestUsageRecorder;
+import com.android.build.shrinker.usages.ResourceUsageRecorder;
+import com.android.build.shrinker.usages.ToolsAttributeUsageRecorder;
+import com.android.utils.FileUtils;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.nio.file.FileSystem;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.zip.ZipFile;
+import javax.xml.parsers.ParserConfigurationException;
+import org.xml.sax.SAXException;
+
+public class ResourceShrinkerCli {
+
+    private static final String INPUT_ARG = "--input";
+    private static final String DEX_INPUT_ARG = "--dex_input";
+    private static final String OUTPUT_ARG = "--output";
+    private static final String RES_ARG = "--raw_resources";
+    private static final String HELP_ARG = "--help";
+    private static final String PRINT_USAGE_LOG = "--print_usage_log";
+
+    private static final String ANDROID_MANIFEST_XML = "AndroidManifest.xml";
+    private static final String RESOURCES_PB = "resources.pb";
+    private static final String RES_FOLDER = "res";
+
+    private static class Options {
+        private String input;
+        private final List<String> dex_inputs = new ArrayList<>();
+        private String output;
+        private String usageLog;
+        private final List<String> rawResources = new ArrayList<>();
+        private boolean help;
+
+        private Options() {}
+
+        public static Options parseOptions(String[] args) {
+            Options options = new Options();
+            for (int i = 0; i < args.length; i++) {
+                String arg = args[i];
+                if (arg.startsWith(INPUT_ARG)) {
+                    i++;
+                    if (i == args.length) {
+                        throw new ResourceShrinkingFailedException("No argument given for input");
+                    }
+                    if (options.input != null) {
+                        throw new ResourceShrinkingFailedException(
+                                "More than one input not supported");
+                    }
+                    options.input = args[i];
+                } else if (arg.startsWith(OUTPUT_ARG)) {
+                    i++;
+                    if (i == args.length) {
+                        throw new ResourceShrinkingFailedException("No argument given for output");
+                    }
+                    if (options.output != null) {
+                        throw new ResourceShrinkingFailedException(
+                                "More than one output not supported");
+                    }
+                    options.output = args[i];
+                } else if (arg.startsWith(DEX_INPUT_ARG)) {
+                    i++;
+                    if (i == args.length) {
+                        throw new ResourceShrinkingFailedException(
+                                "No argument given for dex_input");
+                    }
+                    options.dex_inputs.add(args[i]);
+                } else if (arg.startsWith(PRINT_USAGE_LOG)) {
+                    i++;
+                    if (i == args.length) {
+                        throw new ResourceShrinkingFailedException(
+                                "No argument given for usage log");
+                    }
+                    if (options.usageLog != null) {
+                        throw new ResourceShrinkingFailedException(
+                                "More than usage log not supported");
+                    }
+                    options.usageLog = args[i];
+                } else if (arg.startsWith(RES_ARG)) {
+                    i++;
+                    if (i == args.length) {
+                        throw new ResourceShrinkingFailedException(
+                                "No argument given for raw_resources");
+                    }
+                    options.rawResources.add(args[i]);
+                } else if (arg.equals(HELP_ARG)) {
+                    options.help = true;
+                } else {
+                    throw new ResourceShrinkingFailedException("Unknown argument " + arg);
+                }
+            }
+            return options;
+        }
+
+        public String getInput() {
+            return input;
+        }
+
+        public String getOutput() {
+            return output;
+        }
+
+        public String getUsageLog() {
+            return usageLog;
+        }
+
+        public List<String> getRawResources() {
+            return rawResources;
+        }
+
+        public boolean isHelp() {
+            return help;
+        }
+    }
+
+    public static void main(String[] args) {
+        run(args);
+    }
+
+    protected static ResourceShrinkerImpl run(String[] args) {
+        try {
+            Options options = Options.parseOptions(args);
+            if (options.isHelp()) {
+                printUsage();
+                return null;
+            }
+            validateOptions(options);
+            ResourceShrinkerImpl resourceShrinker = runResourceShrinking(options);
+            return resourceShrinker;
+        } catch (IOException | ParserConfigurationException | SAXException e) {
+            throw new ResourceShrinkingFailedException(
+                    "Failed running resource shrinking: " + e.getMessage(), e);
+        }
+    }
+
+    private static ResourceShrinkerImpl runResourceShrinking(Options options)
+            throws IOException, ParserConfigurationException, SAXException {
+        validateInput(options.getInput());
+        List<ResourceUsageRecorder> resourceUsageRecorders = new ArrayList<>();
+        for (String dexInput : options.dex_inputs) {
+            validateFileExists(dexInput);
+            resourceUsageRecorders.add(
+                    new DexUsageRecorder(
+                            FileUtils.createZipFilesystem(Paths.get(dexInput)).getPath("")));
+        }
+        Path protoApk = Paths.get(options.getInput());
+        Path protoApkOut = Paths.get(options.getOutput());
+        FileSystem fileSystemProto = FileUtils.createZipFilesystem(protoApk);
+        resourceUsageRecorders.add(new DexUsageRecorder(fileSystemProto.getPath("")));
+        resourceUsageRecorders.add(
+                new ProtoAndroidManifestUsageRecorder(
+                        fileSystemProto.getPath(ANDROID_MANIFEST_XML)));
+        // If the apk contains a raw folder, find keep rules in there
+        if (new ZipFile(options.getInput())
+                .stream().anyMatch(zipEntry -> zipEntry.getName().startsWith("res/raw"))) {
+            Path rawPath = fileSystemProto.getPath("res", "raw");
+            resourceUsageRecorders.add(new ToolsAttributeUsageRecorder(rawPath));
+        }
+        ResourcesGatherer gatherer =
+                new ProtoResourceTableGatherer(fileSystemProto.getPath(RESOURCES_PB));
+        ProtoResourcesGraphBuilder res =
+                new ProtoResourcesGraphBuilder(
+                        fileSystemProto.getPath(RES_FOLDER), fileSystemProto.getPath(RESOURCES_PB));
+        ResourceShrinkerImpl resourceShrinker =
+                new ResourceShrinkerImpl(
+                        List.of(gatherer),
+                        null,
+                        resourceUsageRecorders,
+                        List.of(res),
+                        options.usageLog != null
+                                ? new FileReporter(Paths.get(options.usageLog).toFile())
+                                : NoDebugReporter.INSTANCE,
+                        false, // TODO(b/245721267): Add support for bundles
+                        true);
+        resourceShrinker.analyze();
+
+        resourceShrinker.rewriteResourcesInApkFormat(
+                protoApk.toFile(), protoApkOut.toFile(), LinkedResourcesFormat.PROTO);
+        return resourceShrinker;
+    }
+
+    private static void validateInput(String input) throws IOException {
+        ZipFile zipfile = new ZipFile(input);
+        if (zipfile.getEntry(ANDROID_MANIFEST_XML) == null) {
+            throw new ResourceShrinkingFailedException(
+                    "Input must include " + ANDROID_MANIFEST_XML);
+        }
+        if (zipfile.getEntry(RESOURCES_PB) == null) {
+            throw new ResourceShrinkingFailedException(
+                    "Input must include "
+                            + RESOURCES_PB
+                            + ". Did you not convert the input apk"
+                            + " to proto?");
+        }
+        if (zipfile.stream().noneMatch(zipEntry -> zipEntry.getName().startsWith(RES_FOLDER))) {
+            throw new ResourceShrinkingFailedException(
+                    "Input must include a " + RES_FOLDER + " folder");
+        }
+    }
+
+    private static void validateFileExists(String file) {
+        if (!Paths.get(file).toFile().exists()) {
+            throw new RuntimeException("Can't find file: " + file);
+        }
+    }
+
+    private static void validateOptions(Options options) {
+        if (options.getInput() == null) {
+            throw new ResourceShrinkingFailedException("No input given.");
+        }
+        if (options.getOutput() == null) {
+            throw new ResourceShrinkingFailedException("No output destination given.");
+        }
+        validateFileExists(options.getInput());
+        for (String rawResource : options.getRawResources()) {
+            validateFileExists(rawResource);
+        }
+    }
+
+    private static void printUsage() {
+        PrintStream out = System.err;
+        out.println("Usage:");
+        out.println("  resourceshrinker ");
+        out.println("    --input <input-file>, container with manifest, resources table and res");
+        out.println("      folder. May contain dex.");
+        out.println("    --dex_input <input-file> Container with dex files (only dex will be ");
+        out.println("       handled if this contains other files. Several --dex_input arguments");
+        out.println("       are supported");
+        out.println("    --output <output-file>");
+        out.println("    --raw_resource <xml-file or res directory>");
+        out.println("      optional, more than one raw_resoures argument might be given");
+        out.println("    --help prints this help message");
+    }
+
+    private static class ResourceShrinkingFailedException extends RuntimeException {
+        public ResourceShrinkingFailedException(String message) {
+            super(message);
+        }
+
+        public ResourceShrinkingFailedException(String message, Exception e) {
+            super(message, e);
+        }
+    }
+}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinkerImpl.kt b/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinkerImpl.kt
new file mode 100644
index 0000000..1136085
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinkerImpl.kt
@@ -0,0 +1,316 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.build.shrinker
+
+import com.android.SdkConstants.DOT_9PNG
+import com.android.SdkConstants.DOT_PNG
+import com.android.SdkConstants.DOT_XML
+import com.android.aapt.Resources
+import com.android.build.shrinker.DummyContent.TINY_9PNG
+import com.android.build.shrinker.DummyContent.TINY_9PNG_CRC
+import com.android.build.shrinker.DummyContent.TINY_BINARY_XML
+import com.android.build.shrinker.DummyContent.TINY_BINARY_XML_CRC
+import com.android.build.shrinker.DummyContent.TINY_PNG
+import com.android.build.shrinker.DummyContent.TINY_PNG_CRC
+import com.android.build.shrinker.DummyContent.TINY_PROTO_XML
+import com.android.build.shrinker.DummyContent.TINY_PROTO_XML_CRC
+import com.android.build.shrinker.gatherer.ResourcesGatherer
+import com.android.build.shrinker.graph.ResourcesGraphBuilder
+import com.android.build.shrinker.obfuscation.ObfuscationMappingsRecorder
+import com.android.build.shrinker.usages.ResourceUsageRecorder
+import com.android.ide.common.resources.findUnusedResources
+import com.android.ide.common.resources.usage.ResourceStore
+import com.android.ide.common.resources.usage.ResourceUsageModel.Resource
+import com.android.resources.FolderTypeRelationship
+import com.android.resources.ResourceFolderType
+import com.android.resources.ResourceType
+import com.google.common.io.ByteStreams
+import com.google.common.io.Files
+import java.io.BufferedOutputStream
+import java.io.File
+import java.io.FileOutputStream
+import java.io.IOException
+import java.io.InputStream
+import java.util.jar.JarEntry
+import java.util.jar.JarOutputStream
+import java.util.zip.CRC32
+import java.util.zip.ZipEntry
+import java.util.zip.ZipFile
+
+/**
+ * Unit that analyzes all resources (after resource merging, compilation and code shrinking has
+ * been completed) and figures out which resources are unused, and replaces them with dummy content
+ * inside zip archive file.
+ *
+ * Resource shrinker implementation that allows to customize:
+ * <ul>
+ *     <li>application resource gatherer (from R files, resource tables, etc);
+ *     <li>recorder for mappings from obfuscated class/methods to original class/methods;
+ *     <li>sources from which resource usages are recorded (Dex files, compiled JVM classes,
+ *         AndroidManifests, etc);
+ *     <li>resources graph builder that connects resources dependent on each other (analyzing
+ *         raw resources content in XML, HTML, CSS, JS, analyzing resource content in proto
+ *         compiled format);
+ * </ul>
+ */
+class ResourceShrinkerImpl(
+    private val resourcesGatherers: List<ResourcesGatherer>,
+    private val obfuscationMappingsRecorder: ObfuscationMappingsRecorder?,
+    private val usageRecorders: List<ResourceUsageRecorder>,
+    private val graphBuilders: List<ResourcesGraphBuilder>,
+    private val debugReporter: ShrinkerDebugReporter,
+    val supportMultipackages: Boolean,
+    private val usePreciseShrinking: Boolean
+) : ResourceShrinker {
+    val model = ResourceShrinkerModel(debugReporter, supportMultipackages)
+    private lateinit var unused: List<Resource>
+
+    override fun analyze() {
+        resourcesGatherers.forEach { it.gatherResourceValues(model) }
+        obfuscationMappingsRecorder?.recordObfuscationMappings(model)
+        usageRecorders.forEach { it.recordUsages(model) }
+        graphBuilders.forEach { it.buildGraph(model) }
+
+        model.resourceStore.processToolsAttributes()
+        model.keepPossiblyReferencedResources()
+
+        debugReporter.debug { model.resourceStore.dumpResourceModel() }
+
+        unused = findUnusedResources(model.resourceStore.resources) { roots ->
+            debugReporter.debug { "The root reachable resources are:" }
+            debugReporter.debug { roots.joinToString("\n", transform = { " $it" }) }
+        }
+        debugReporter.debug { "Unused resources are: " }
+        debugReporter.debug { unused.joinToString("\n", transform = { " $it" })}
+
+    }
+
+    override fun close() {
+        debugReporter.close()
+    }
+
+    override fun getUnusedResourceCount(): Int {
+        return unused.size
+    }
+
+    override fun rewriteResourcesInApkFormat(
+        source: File,
+        dest: File,
+        format: LinkedResourcesFormat
+    ) {
+        rewriteResourceZip(source, dest, ApkArchiveFormat(model.resourceStore, format))
+    }
+
+    override fun rewriteResourcesInBundleFormat(
+        source: File,
+        dest: File,
+        moduleNameToPackageNameMap: Map<String, String>
+    ) {
+        rewriteResourceZip(
+            source,
+            dest,
+            BundleArchiveFormat(model.resourceStore, moduleNameToPackageNameMap)
+        )
+    }
+
+    private fun rewriteResourceZip(source: File, dest: File, format: ArchiveFormat) {
+        if (dest.exists() && !dest.delete()) {
+            throw IOException("Could not delete $dest")
+        }
+        JarOutputStream(BufferedOutputStream(FileOutputStream(dest))).use { zos ->
+            ZipFile(source).use { zip ->
+                // Rather than using Deflater.DEFAULT_COMPRESSION we use 9 here,  since that seems
+                // to match the compressed sizes we observe in source .ap_ files encountered by the
+                // resource shrinker:
+                zos.setLevel(9)
+                zip.entries().asSequence().forEach {
+                    if (format.fileIsNotReachable(it)) {
+                        // If we don't use precise shrinking we don't remove the files, see:
+                        // https://b.corp.google.com/issues/37010152
+                        if (!usePreciseShrinking) {
+                            replaceWithDummyEntry(zos, it, format.resourcesFormat)
+                        }
+                    } else if (it.name.endsWith("resources.pb") && usePreciseShrinking) {
+                            removeResourceUnusedTableEntries(zip.getInputStream(it), zos, it)
+                    } else {
+                        copyToOutput(zip.getInputStream(it), zos, it)
+                    }
+                }
+            }
+        }
+        // If net negative, copy original back. This is unusual, but can happen
+        // in some circumstances, such as the one described in
+        // https://plus.google.com/+SaidTahsinDane/posts/X9sTSwoVUhB
+        // "Removed unused resources: Binary resource data reduced from 588KB to 595KB: Removed -1%"
+        // Guard against that, and worst case, just use the original.
+        val before = source.length()
+        val after = dest.length()
+        if (after > before) {
+            debugReporter.info {
+                "Resource shrinking did not work (grew from $before to $after); using original " +
+                "instead"
+            }
+            Files.copy(source, dest)
+        }
+    }
+
+    private fun removeResourceUnusedTableEntries(zis: InputStream,
+                                                 zos: JarOutputStream,
+                                                 srcEntry: ZipEntry) {
+        val resourceIdsToRemove =
+            model.resourceStore.resources.filter { !it.isReachable }.map { it.value }.toList()
+        val shrunkenResourceTable = Resources.ResourceTable.parseFrom(zis)
+                .nullOutEntriesWithIds(resourceIdsToRemove)
+        val bytes = shrunkenResourceTable.toByteArray()
+        val outEntry = JarEntry(srcEntry.name)
+        if (srcEntry.time != -1L) {
+            outEntry.time = srcEntry.time
+        }
+        if (srcEntry.method == JarEntry.STORED) {
+            outEntry.method = JarEntry.STORED
+            outEntry.size = bytes.size.toLong()
+            val crc = CRC32()
+            crc.update(bytes, 0, bytes.size)
+            outEntry.crc = crc.getValue()
+        }
+        zos.putNextEntry(outEntry)
+        zos.write(bytes)
+        zos.closeEntry()
+    }
+
+    /** Replaces the given entry with a minimal valid file of that type.  */
+    private fun replaceWithDummyEntry(
+        zos: JarOutputStream,
+        entry: ZipEntry,
+        format: LinkedResourcesFormat
+    ) {
+        // Create a new entry so that the compressed len is recomputed.
+        val name = entry.name
+        val (bytes, crc) = when {
+            // DOT_9PNG (.9.png) must be always before DOT_PNG (.png)
+            name.endsWith(DOT_9PNG) -> TINY_9PNG to TINY_9PNG_CRC
+            name.endsWith(DOT_PNG) -> TINY_PNG to TINY_PNG_CRC
+            name.endsWith(DOT_XML) && format == LinkedResourcesFormat.BINARY ->
+                TINY_BINARY_XML to TINY_BINARY_XML_CRC
+            name.endsWith(DOT_XML) && format == LinkedResourcesFormat.PROTO ->
+                TINY_PROTO_XML to TINY_PROTO_XML_CRC
+            else -> ByteArray(0) to 0L
+        }
+
+        val outEntry = JarEntry(name)
+        if (entry.time != -1L) {
+            outEntry.time = entry.time
+        }
+        if (entry.method == JarEntry.STORED) {
+            outEntry.method = JarEntry.STORED
+            outEntry.size = bytes.size.toLong()
+            outEntry.crc = crc
+        }
+        zos.putNextEntry(outEntry)
+        zos.write(bytes)
+        zos.closeEntry()
+        debugReporter.info {
+            "Skipped unused resource $name: ${entry.size} bytes (replaced with small dummy file " +
+            "of size ${bytes.size} bytes)"
+        }
+    }
+
+    private fun copyToOutput(zis: InputStream, zos: JarOutputStream, entry: ZipEntry) {
+        // We can't just compress all files; files that are not compressed in the source .ap_ file
+        // must be left uncompressed here, since for example RAW files need to remain uncompressed
+        // in the APK such that they can be mmap'ed at runtime.
+        // Preserve the STORED method of the input entry.
+        val outEntry = when (entry.method) {
+            JarEntry.STORED -> JarEntry(entry)
+            else -> JarEntry(entry.name)
+        }
+        if (entry.time != -1L) {
+            outEntry.time = entry.time
+        }
+        zos.putNextEntry(outEntry)
+        if (!entry.isDirectory) {
+            zos.write(ByteStreams.toByteArray(zis))
+        }
+        zos.closeEntry()
+    }
+}
+
+private interface ArchiveFormat {
+    val resourcesFormat: LinkedResourcesFormat
+    fun fileIsNotReachable(entry: ZipEntry): Boolean
+}
+
+private class ApkArchiveFormat(
+    private val store: ResourceStore,
+    override val resourcesFormat: LinkedResourcesFormat
+) : ArchiveFormat {
+
+    override fun fileIsNotReachable(entry: ZipEntry): Boolean {
+        if (entry.isDirectory || !entry.name.startsWith("res/")) {
+            return false
+        }
+        val (_, folder, name) = entry.name.split('/', limit = 3)
+        return !store.isJarPathReachable(folder, name)
+    }
+}
+
+private class BundleArchiveFormat(
+    private val store: ResourceStore,
+    private val moduleNameToPackageName: Map<String, String>
+) : ArchiveFormat {
+
+    override val resourcesFormat = LinkedResourcesFormat.PROTO
+
+    override fun fileIsNotReachable(entry: ZipEntry): Boolean {
+        val module = entry.name.substringBefore('/')
+        val packageName = moduleNameToPackageName[module]
+        if (entry.isDirectory || packageName == null || !entry.name.startsWith("$module/res/")) {
+            return false
+        }
+        val (_, _, folder, name) = entry.name.split('/', limit = 4)
+        return !store.isJarPathReachable(folder, name)
+    }
+}
+
+private fun ResourceStore.isJarPathReachable(
+    folder: String,
+    name: String
+): Boolean {
+    val folderType = ResourceFolderType.getFolderType(folder) ?: return true
+    val resourceName = name.substringBefore('.')
+    // Bundle format has a restriction: in case the same resource is duplicated in multiple modules
+    // its content should be the same in all of them. This restriction means that we can't replace
+    // resource with dummy content if its duplicate is used in some module.
+    return FolderTypeRelationship.getRelatedResourceTypes(folderType)
+        .filterNot { it == ResourceType.ID }
+        .flatMap { getResources(it, resourceName) }
+        .any { it.isReachable }
+}
+
+private fun ResourceStore.getResourceId(
+    folder: String,
+    name: String
+): Int {
+    val folderType = ResourceFolderType.getFolderType(folder) ?: return -1
+    val resourceName = name.substringBefore('.')
+    return FolderTypeRelationship.getRelatedResourceTypes(folderType)
+        .filterNot { it == ResourceType.ID }
+        .flatMap { getResources(it, resourceName) }
+        .map { it.value }
+        .getOrElse(0) { -1 }
+
+}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinkerModel.java b/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinkerModel.java
new file mode 100644
index 0000000..ce1b39f
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinkerModel.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.build.shrinker;
+
+import com.android.aapt.Resources.ResourceTable;
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.build.shrinker.obfuscation.ObfuscatedClasses;
+import com.android.ide.common.resources.usage.ResourceStore;
+import com.android.ide.common.resources.usage.ResourceUsageModel.Resource;
+import com.android.resources.ResourceType;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Represents resource shrinker state that is shared between shrinker stages ResourcesGatherer,
+ * ObfuscationMappingsRecorder, ResourceUsageRecorder, ResourcesGraphBuilder and each stage
+ * contributes an information via changing its state.
+ */
+public class ResourceShrinkerModel {
+
+    private final ShrinkerDebugReporter debugReporter;
+
+    private final ResourceStore resourceStore;
+    private ObfuscatedClasses obfuscatedClasses = ObfuscatedClasses.NO_OBFUSCATION;
+
+    private final Set<String> strings = Sets.newHashSetWithExpectedSize(300);
+
+    private final Map<String, ResourceTable> resourceTableCache = Maps.newHashMap();
+
+    private boolean foundGetIdentifier = false;
+    private boolean foundWebContent = false;
+
+    public ResourceShrinkerModel(
+            ShrinkerDebugReporter debugReporter, boolean supportMultipackages) {
+        this.debugReporter = debugReporter;
+        resourceStore = new ResourceStore(supportMultipackages);
+    }
+
+    @NonNull
+    public ShrinkerDebugReporter getDebugReporter() {
+        return debugReporter;
+    }
+
+    @NonNull
+    public ResourceStore getResourceStore() {
+        return resourceStore;
+    }
+
+    @NonNull
+    public ObfuscatedClasses getObfuscatedClasses() {
+        return obfuscatedClasses;
+    }
+
+    /** Reports recorded mappings from obfuscated classes to original classes */
+    public void setObfuscatedClasses(@NonNull ObfuscatedClasses obfuscatedClasses) {
+        this.obfuscatedClasses = obfuscatedClasses;
+    }
+
+    /** Adds a new gathered resource to model. */
+    @NonNull
+    public Resource addResource(
+            @NonNull ResourceType type,
+            @Nullable String packageName,
+            @NonNull String name,
+            @Nullable String value) {
+        int intValue = -1;
+        try {
+            intValue = value != null ? Integer.decode(value) : -1;
+        } catch (NumberFormatException e) {
+            // ignore
+        }
+        return addResource(type, packageName, name, intValue);
+    }
+
+    /** Adds a new gathered resource to model. */
+    @NonNull
+    public Resource addResource(
+            @NonNull ResourceType type,
+            @Nullable String packageName,
+            @NonNull String name,
+            int value) {
+        return resourceStore.addResource(new Resource(packageName, type, name, value));
+    }
+
+    /** Adds string constant found in code to strings pool. */
+    public void addStringConstant(@NonNull String string) {
+        strings.add(string);
+    }
+
+    /** Returns is reference to {@code Resources#getIdentifier} is found in code. */
+    public boolean isFoundGetIdentifier() {
+        return foundGetIdentifier;
+    }
+
+    /** Reports that reference to {@code Resources#getIdentifier} is found in code. */
+    public void setFoundGetIdentifier(boolean foundGetIdentifier) {
+        this.foundGetIdentifier = foundGetIdentifier;
+    }
+
+    /** Returns is web content is found in code. */
+    public boolean isFoundWebContent() {
+        return foundWebContent;
+    }
+
+    /** Reports that reference to web content is found in code. */
+    public void setFoundWebContent(boolean foundWebContent) {
+        this.foundWebContent = foundWebContent;
+    }
+
+    /** Returns all string constants gathered from compiled classes. */
+    public Set<String> getStrings() {
+        return strings;
+    }
+
+    /**
+     * Mark resources that match string constants as reachable in case invocation of
+     * {@code Resources#getIdentifier} or web content is found in code and safe mode in enabled.
+     */
+    public void keepPossiblyReferencedResources() {
+        if (strings.isEmpty()
+                || !resourceStore.getSafeMode()
+                || (!foundGetIdentifier && !foundWebContent)) {
+            // No calls to android.content.res.Resources#getIdentifier; or user specifically asked
+            // for us not to guess resources to keep
+            return;
+        }
+
+        debugReporter.debug(() -> ""
+                        + "android.content.res.Resources#getIdentifier present: " + foundGetIdentifier + "\n"
+                        + "Web content present: " + foundWebContent + "\n"
+                        + "Referenced Strings:\n"
+                        + strings.stream()
+                            .map(s -> s.trim().replace("\n", "\\n"))
+                            .filter(s -> !s.isEmpty())
+                            .map(s -> s.length() > 40 ? s.substring(0, 37) + "..." : s)
+                            .collect(Collectors.joining("\n"))
+        );
+
+        new PossibleResourcesMarker(debugReporter, resourceStore, strings, foundWebContent)
+                .markPossibleResourcesReachable();
+    }
+
+    /**
+     * Reads resource table from specified path and stores it in cache to be able to reuse it if
+     * the same resource table is requested by another unit.
+     */
+    public ResourceTable readResourceTable(Path resourceTablePath) {
+        return resourceTableCache.computeIfAbsent(resourceTablePath.toString(), (path) -> {
+            try {
+                return ResourceTable.parseFrom(Files.readAllBytes(resourceTablePath));
+            } catch (IOException e) {
+                throw new UncheckedIOException(e);
+            }
+        });
+    }
+}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/ResourceTableUtil.kt b/src/resourceshrinker/java/com/android/build/shrinker/ResourceTableUtil.kt
new file mode 100644
index 0000000..69952ef
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/ResourceTableUtil.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.build.shrinker
+
+import com.android.aapt.Resources
+
+internal fun Resources.ResourceTable.entriesSequence(): Sequence<EntryWrapper> = sequence {
+    for (resourcePackage in packageList) {
+        for (resourceType in resourcePackage.typeList) {
+            for (resourceEntry in resourceType.entryList) {
+                val id = toIdentifier(resourcePackage, resourceType, resourceEntry)
+                yield(
+                    EntryWrapper(id, resourcePackage.packageName, resourceType.name, resourceEntry)
+                )
+            }
+        }
+    }
+}
+
+internal fun Resources.ResourceTable.nullOutEntriesWithIds(ids: List<Int>)
+    : Resources.ResourceTable {
+    if (ids.isEmpty()) {
+        return this
+    }
+    val packageMappings = calculatePackageMappings(ids)
+    val tableBuilder = this.toBuilder()
+    tableBuilder.packageBuilderList.forEach{
+        val typeMappings = packageMappings[it.packageId.id]
+        if (typeMappings != null) {
+            it.typeBuilderList.forEach { type->
+                val entryList = typeMappings[type.typeId.id]
+                if (entryList != null) {
+                    type.entryBuilderList.forEach { entry ->
+                        if (entryList.contains(entry.entryId.id)) {
+                            entry.clearConfigValue()
+                            if (entry.hasOverlayableItem()) {
+                                entry.clearOverlayableItem()
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+    return tableBuilder.build()
+}
+
+private fun calculatePackageMappings(ids: List<Int>): MutableMap<Int, Map<Int, List<Int>>> {
+    val sortedIds = ids.sorted()
+    val packageMapping = mutableMapOf<Int, Map<Int, List<Int>>>()
+    var typeMapping = mutableMapOf<Int, List<Int>>()
+    var entryList = mutableListOf<Int>()
+    var oldPackageId = -1
+    var oldTypeId = -1
+    for (value in sortedIds) {
+        val packageId = packageIdFromIdentifier(value)
+        val typeId = typeIdFromIdentifier(value)
+        val entryId = entryIdFromIdentifier(value)
+        if (packageId != oldPackageId) {
+            typeMapping = mutableMapOf()
+            packageMapping.put(packageId, typeMapping)
+            oldPackageId = packageId
+            oldTypeId = -1
+        }
+        if (typeId != oldTypeId) {
+            entryList = mutableListOf()
+            typeMapping.put(typeId, entryList)
+            oldTypeId = typeId
+        }
+        entryList.add(entryId)
+    }
+    return packageMapping
+}
+
+internal data class EntryWrapper(
+    val id: Int,
+    val packageName: String,
+    val type: String,
+    val entry: Resources.Entry
+)
+
+private fun toIdentifier(
+    resourcePackage: Resources.Package,
+    type: Resources.Type,
+    entry: Resources.Entry
+): Int =
+    (resourcePackage.packageId.id shl 24) or (type.typeId.id shl 16) or entry.entryId.id
+
+private fun packageIdFromIdentifier(
+    identifier: Int
+): Int =
+    identifier shr 24
+
+private fun typeIdFromIdentifier(
+    identifier: Int
+): Int =
+    (identifier and 0x00FF0000) shr 16
+
+private fun entryIdFromIdentifier(
+    identifier: Int
+): Int =
+    (identifier and 0x0000FFFF)
+
+
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/ShrinkerDebugReporter.kt b/src/resourceshrinker/java/com/android/build/shrinker/ShrinkerDebugReporter.kt
new file mode 100644
index 0000000..b592bb8
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/ShrinkerDebugReporter.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.build.shrinker
+
+import java.io.File
+import java.io.PrintWriter
+
+interface ShrinkerDebugReporter : AutoCloseable {
+    fun debug(f: () -> String)
+    fun info(f: () -> String)
+}
+
+object NoDebugReporter : ShrinkerDebugReporter {
+    override fun debug(f: () -> String) = Unit
+
+    override fun info(f: () -> String) = Unit
+
+    override fun close() = Unit
+}
+
+class FileReporter(
+    reportFile: File
+) : ShrinkerDebugReporter {
+    private val writer: PrintWriter = reportFile.let { PrintWriter(it) }
+    override fun debug(f: () -> String) {
+        writer.println(f())
+    }
+
+    override fun info(f: () -> String) {
+        writer.println(f())
+    }
+
+    override fun close() {
+        writer.close()
+    }
+}
+
+class LoggerAndFileDebugReporter(
+    private val logDebug: (String) -> Unit,
+    private val logInfo: (String) -> Unit,
+    reportFile: File?
+) : ShrinkerDebugReporter {
+    private val writer: PrintWriter? = reportFile?.let { PrintWriter(it) }
+
+    override fun debug(f: () -> String) {
+        val message = f()
+        writer?.println(message)
+        logDebug(message)
+    }
+
+    override fun info(f: () -> String) {
+        val message = f()
+        writer?.println(message)
+        logInfo(message)
+    }
+
+    override fun close() {
+        writer?.close()
+    }
+}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/gatherer/ProtoResourceTableGatherer.kt b/src/resourceshrinker/java/com/android/build/shrinker/gatherer/ProtoResourceTableGatherer.kt
new file mode 100644
index 0000000..c3c00eb
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/gatherer/ProtoResourceTableGatherer.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.build.shrinker.gatherer
+
+import com.android.build.shrinker.ResourceShrinkerModel
+import com.android.build.shrinker.entriesSequence
+import com.android.ide.common.resources.resourceNameToFieldName
+import com.android.resources.ResourceType
+import java.nio.file.Path
+
+/**
+ * Gathers application resources from proto compiled resource table. Flattens each resource name to
+ * be compatible with field names in R classes.
+ *
+ * @param resourceTablePath path to resource table in proto format.
+ */
+class ProtoResourceTableGatherer(private val resourceTablePath: Path) : ResourcesGatherer {
+
+    override fun gatherResourceValues(model: ResourceShrinkerModel) {
+        model.readResourceTable(resourceTablePath).entriesSequence()
+            .forEach { (id, packageName, type, entry) ->
+                // We need to flatten resource names here to match fields names in R classes via
+                // invoking ResourcesUtil.resourceNameToFieldName because we need to record R fields
+                // usages in code.
+                ResourceType.fromClassName(type)
+                    ?.takeIf { it != ResourceType.STYLEABLE }
+                    ?.let {
+                        model.addResource(it, packageName, resourceNameToFieldName(entry.name), id)
+                    }
+            }
+    }
+}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/gatherer/ResourcesGatherer.java b/src/resourceshrinker/java/com/android/build/shrinker/gatherer/ResourcesGatherer.java
new file mode 100644
index 0000000..ffe5f5b
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/gatherer/ResourcesGatherer.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.build.shrinker.gatherer;
+
+import com.android.annotations.NonNull;
+import com.android.build.shrinker.ResourceShrinkerModel;
+import java.io.IOException;
+
+/**
+ * Interface for unit that should gather application resources and contribute them to
+ * ResourceShrinkerModel
+ */
+public interface ResourcesGatherer {
+
+  /**
+   * Gathers application resources and contribute them to ResourceShrinkerModel via
+   * ResourceShrinkerModel.addResource
+   */
+  void gatherResourceValues(@NonNull ResourceShrinkerModel model) throws IOException;
+}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/graph/ProtoResourcesGraphBuilder.kt b/src/resourceshrinker/java/com/android/build/shrinker/graph/ProtoResourcesGraphBuilder.kt
new file mode 100644
index 0000000..130e585
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/graph/ProtoResourcesGraphBuilder.kt
@@ -0,0 +1,302 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.build.shrinker.graph
+
+import com.android.aapt.Resources
+import com.android.aapt.Resources.Entry
+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.XmlAttribute
+import com.android.aapt.Resources.XmlElement
+import com.android.aapt.Resources.XmlNode
+import com.android.build.shrinker.ResourceShrinkerModel
+import com.android.build.shrinker.entriesSequence
+import com.android.ide.common.resources.usage.ResourceUsageModel
+import com.android.ide.common.resources.usage.ResourceUsageModel.Resource
+import com.android.ide.common.resources.usage.WebTokenizers
+import com.android.resources.ResourceType
+import com.android.utils.SdkUtils.IMAGE_EXTENSIONS
+import com.google.common.base.Ascii
+import java.io.IOException
+import java.nio.charset.StandardCharsets
+import java.nio.file.Files
+import java.nio.file.Path
+
+/**
+ * Builds resources graph starting from each resource in resource table in proto format and follow
+ * all references to other resources inside inlined values and external files from res/ folder.
+ *
+ * <p>Supports external files in the following formats:
+ * <ul>
+ *     <li>XML files compiled to proto format;
+ *     <li>HTML, CSS, JS files inside res/raw folder;
+ *     <li>Unknown files inside res/raw folder (only looks for 'android_res/<type>/<name>' pattern);
+ * </ul>
+ *
+ * <p>As ID resources don't specify parent-child relations between resources but are just
+ * identifiers for a resource or some part of the resource we don't gather them as references to
+ * examined resource.
+ *
+ * @param resourceRoot path to <module>/res/ folder.
+ * @param resourceTable path to resource table in proto format.
+ */
+class ProtoResourcesGraphBuilder(
+    private val resourceRoot: Path,
+    private val resourceTable: Path
+) : ResourcesGraphBuilder {
+
+    override fun buildGraph(model: ResourceShrinkerModel) {
+        model.readResourceTable(resourceTable).entriesSequence()
+            .map { (id, _, _, entry) ->
+                model.resourceStore.getResource(id)?.let {
+                    ReferencesForResourceFinder(resourceRoot, model, entry, it)
+                }
+            }
+            .filterNotNull()
+            .forEach { it.findReferences() }
+    }
+}
+
+private class ReferencesForResourceFinder(
+    private val resourcesRoot: Path,
+    private val model: ResourceShrinkerModel,
+    private val entry: Entry,
+    private val current: Resource
+) {
+    companion object {
+        /**
+         * 'android_res/' is a synthetic directory for resource references in URL format. For
+         *  example: file:///android_res/raw/intro_page.
+         */
+        private const val ANDROID_RES = "android_res/"
+
+        private const val CONSTRAINT_REFERENCED_IDS = "constraint_referenced_ids"
+
+        private fun Reference.asItem(): Resources.Item =
+            Resources.Item.newBuilder().setRef(this).build()
+    }
+
+    private val webTokenizers: WebTokenizers by lazy {
+        WebTokenizers(object : WebTokenizers.WebTokensCallback {
+            override fun referencedHtmlAttribute(tag: String?, attribute: String?, value: String) {
+                if (attribute == "href" || attribute == "src") {
+                    referencedUrl(value)
+                }
+            }
+
+            override fun referencedJsString(jsString: String) {
+                referencedStringFromWebContent(jsString)
+            }
+
+            override fun referencedCssUrl(url: String) {
+                referencedUrl(url)
+            }
+
+            private fun referencedUrl(url: String) {
+                // 1. if url contains '/' try to find resources from this web url.
+                // 2. if there is no '/' it might be just relative reference to another resource.
+                val resources = when {
+                    url.contains('/') -> model.resourceStore.getResourcesFromWebUrl(url)
+                    else ->
+                        model.resourceStore.getResources(ResourceType.RAW, url.substringBefore('.'))
+                }
+                if (resources.isNotEmpty()) {
+                    resources.forEach { current.addReference(it) }
+                } else {
+                    // if there is no resources found by provided url just gather this string as
+                    // found inside web content to process it afterwards.
+                    referencedStringFromWebContent(url)
+                }
+            }
+
+            private fun referencedStringFromWebContent(string: String) {
+                if (string.isNotEmpty() && string.length <= 80) {
+                    model.addStringConstant(string)
+                    model.isFoundWebContent = true
+                }
+            }
+        })
+    }
+
+    fun findReferences() {
+        // Walk through all values of the entry and find all Item instances that may reference
+        // other resources in resource table itself or specify external files that should be
+        // analyzed for references.
+        entry.configValueList.asSequence()
+            .map { it.value }
+            .flatMap { value ->
+                val compoundValue = value.compoundValue
+                // compoundValue.attr and compoundValue.styleable are skipped, attr defines
+                // references to ID resources only, but ID and STYLEABLE resources are not supported
+                // by shrinker.
+                when {
+                    value.hasItem() ->
+                        sequenceOf(value.item)
+                    compoundValue.hasStyle() ->
+                        sequenceOf(compoundValue.style.parent.asItem()) +
+                                compoundValue.style.entryList.asSequence().flatMap {
+                                    sequenceOf(
+                                        it.item,
+                                        it.key.asItem()
+                                    )
+                                }
+                    compoundValue.hasArray() ->
+                        compoundValue.array.elementList.asSequence().map { it.item }
+                    compoundValue.hasPlural() ->
+                        compoundValue.plural.entryList.asSequence().map { it.item }
+                    else -> emptySequence()
+                }
+            }
+            .forEach { findFromItem(it) }
+    }
+
+    private fun findFromItem(item: Resources.Item) {
+        try {
+            when {
+                item.hasRef() -> findFromReference(item.ref)
+                item.hasFile() && item.file.path.startsWith("res/") -> findFromFile(item.file)
+            }
+        } catch (e: IOException) {
+            model.debugReporter.debug { "File '${item.file.path}' can not be processed. Skipping." }
+        }
+    }
+
+    private fun findFromReference(reference: Reference) {
+        // Reference object may have id of referenced resource, in this case prefer resolved id.
+        // In case id is not provided try to find referenced resource by name. Name is converted
+        // to resource url here, because name in resource table is not normalized to R style field
+        // and to find it we need normalize it first (for example, in case name in resource table is
+        // MyStyle.Child in R file it is R.style.MyStyle_child).
+        val referencedResources = when {
+            reference.id != 0 -> listOf(model.resourceStore.getResource(reference.id))
+            reference.name.isNotEmpty() ->
+                model.resourceStore.getResourcesFromUrl("@${reference.name}")
+            else -> emptyList()
+        }
+        // IDs are not supported by shrinker for now, just skip it.
+        referencedResources.asSequence()
+            .filterNotNull()
+            .filter { it.type != ResourceType.ID }
+            .forEach { current.addReference(it) }
+    }
+
+    private fun findFromFile(file: FileReference) {
+        val path = resourcesRoot.resolve(file.path.substringAfter("res/"))
+        val bytes: ByteArray by lazy { Files.readAllBytes(path) }
+        val content: String by lazy { String(bytes, StandardCharsets.UTF_8) }
+        val extension = Ascii.toLowerCase(path.fileName.toString()).substringAfter('.')
+        when {
+            file.type == PROTO_XML -> fillFromXmlNode(XmlNode.parseFrom(bytes))
+            extension in listOf("html", "htm") -> webTokenizers.tokenizeHtml(content)
+            extension == "css" -> webTokenizers.tokenizeCss(content)
+            extension == "js" -> webTokenizers.tokenizeJs(content)
+            extension !in IMAGE_EXTENSIONS -> maybeAndroidResUrl(content, markAsReachable = false)
+        }
+    }
+
+    private fun fillFromXmlNode(node: XmlNode) {
+        // Check for possible reference as 'android_res/<type>/<name>' pattern inside element text.
+        if (current.type == ResourceType.XML) {
+            maybeAndroidResUrl(node.text, markAsReachable = true)
+        }
+        // Check special xml element <rawPathResId> which provides reference to res/raw/ apk
+        // resource for wear application. Applies to all XML files for now but might be re-scoped
+        // to only apply to <wearableApp> XMLs.
+        maybeWearAppReference(node.element)
+        node.element.attributeList.forEach { fillFromAttribute(it) }
+        node.element.childList.forEach { fillFromXmlNode(it) }
+    }
+
+    private fun fillFromAttribute(attribute: XmlAttribute) {
+        if (attribute.name == CONSTRAINT_REFERENCED_IDS) {
+            fillFromConstraintReferencedIds(attribute.value)
+        }
+        if (attribute.hasCompiledItem()) {
+            findFromItem(attribute.compiledItem)
+        }
+        // Check for possible reference as 'android_res/<type>/<name>' pattern inside attribute val.
+        if (current.type == ResourceType.XML) {
+            maybeAndroidResUrl(attribute.value, markAsReachable = true)
+        }
+    }
+
+    private fun fillFromConstraintReferencedIds(value: String?) {
+        value
+            ?.split(",")
+            ?.map { it.trim() }
+            ?.forEach {
+                model.resourceStore.getResources(ResourceType.ID, it)
+                    .forEach(ResourceUsageModel::markReachable)
+            }
+    }
+
+    private fun maybeAndroidResUrl(text: String, markAsReachable: Boolean) {
+        findAndroidResReferencesInText(text)
+            .map { it.split('/', limit = 2) }
+            .filter { it.size == 2 }
+            .map { (dir, fileName) ->
+                Pair(
+                    ResourceType.fromFolderName(dir.substringBefore('-')),
+                    fileName.substringBefore('.')
+                )
+            }
+            .filter { (type, _) -> type != null }
+            .flatMap { (type, name) -> model.resourceStore.getResources(type!!, name).asSequence() }
+            .forEach {
+                if (markAsReachable) {
+                    ResourceUsageModel.markReachable(it)
+                } else {
+                    current.addReference(it)
+                }
+            }
+    }
+
+    private fun maybeWearAppReference(element: XmlElement) {
+        if (element.name == "rawPathResId") {
+            val rawResourceName = element.childList
+                .map { it.text }
+                .joinToString(separator = "")
+                .trim()
+
+            model.resourceStore.getResources(ResourceType.RAW, rawResourceName)
+                .forEach { current.addReference(it) }
+        }
+    }
+
+    /**
+     * Splits input text to parts that starts with 'android_res/' and returns sequence of strings
+     * between 'android_res/' occurrences and first whitespace after it. This method is used instead
+     * of {@link CharSequence#splitToSequence} because does not spawn full substrings between
+     * 'android_res/' in memory when text is big enough.
+     */
+    private fun findAndroidResReferencesInText(text: String): Sequence<String> = sequence {
+        var start = 0
+        while (start < text.length) {
+            start = text.indexOf(ANDROID_RES, start)
+            if (start == -1) {
+                break
+            }
+            var end = start + ANDROID_RES.length
+            while (end < text.length && !Character.isWhitespace(text[end])) {
+                end++
+            }
+            yield(text.substring(start + ANDROID_RES.length, end))
+            start = end
+        }
+    }
+}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/graph/ResourcesGraphBuilder.java b/src/resourceshrinker/java/com/android/build/shrinker/graph/ResourcesGraphBuilder.java
new file mode 100644
index 0000000..e09b0eb
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/graph/ResourcesGraphBuilder.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.build.shrinker.graph;
+
+import com.android.annotations.NonNull;
+import com.android.build.shrinker.ResourceShrinkerModel;
+import java.io.IOException;
+
+/**
+ * Interface for unit that should find references between resources which are gathered inside
+ * ResourceShrinkerModel.
+ */
+public interface ResourcesGraphBuilder {
+
+  /**
+   * Finds references between resources and connects them. May introduce and contribute new
+   * resources to ResourceShrinkerModel.
+   */
+  void buildGraph(@NonNull ResourceShrinkerModel model) throws IOException;
+}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/obfuscation/ObfuscatedClasses.kt b/src/resourceshrinker/java/com/android/build/shrinker/obfuscation/ObfuscatedClasses.kt
new file mode 100644
index 0000000..3cf757d
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/obfuscation/ObfuscatedClasses.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.build.shrinker.obfuscation
+
+import com.google.common.collect.ImmutableMap
+
+/** Contains mappings between obfuscated classes and methods to original ones. */
+class ObfuscatedClasses private constructor(builder: Builder) {
+
+    companion object {
+        @JvmField
+        val NO_OBFUSCATION = Builder().build()
+    }
+
+    private val obfuscatedClasses = ImmutableMap.copyOf(builder.obfuscatedClasses)
+    private val obfuscatedMethods = ImmutableMap.copyOf(builder.obfuscatedMethods)
+
+    fun resolveOriginalMethod(obfuscatedMethod: ClassAndMethod): ClassAndMethod {
+        return obfuscatedMethods.getOrElse(obfuscatedMethod) {
+            val realClassName = obfuscatedClasses[obfuscatedMethod.className] ?: obfuscatedMethod.className
+            ClassAndMethod(realClassName, obfuscatedMethod.methodName)
+        }
+    }
+
+    fun resolveOriginalClass(obfuscatedClass: String): String {
+        return obfuscatedClasses[obfuscatedClass] ?: obfuscatedClass
+    }
+
+    /**
+     * Builder that allows to build obfuscated mappings in a way when next method mapping is added
+     * to previous class mapping. Example:
+     * builder
+     *   .addClassMapping(Pair(classA, obfuscatedClassA))
+     *   .addMethodMapping(Pair(classAMethod1, obfuscatedClassAMethod1))
+     *   .addMethodMapping(Pair(classAMethod2, obfuscatedClassAMethod2))
+     *   .addClassMapping(Pair(classB, obfuscatedClassB))
+     *   .addMethodMapping(Pair(classBMethod1, obfuscatedClassBMethod1))
+     */
+    class Builder {
+
+        val obfuscatedClasses: MutableMap<String, String> = mutableMapOf()
+        val obfuscatedMethods: MutableMap<ClassAndMethod, ClassAndMethod> = mutableMapOf()
+
+        var currentClassMapping: Pair<String, String>? = null
+
+        /**
+         * Adds class mapping: original class name -> obfuscated class name.
+         *
+         * @param mapping Pair(originalClassName, obfuscatedClassName)
+         */
+        fun addClassMapping(mapping: Pair<String, String>): Builder {
+            currentClassMapping = mapping
+            obfuscatedClasses += Pair(mapping.second, mapping.first)
+            return this
+        }
+
+        /**
+         * Adds method mapping: original method name -> obfuscated method name to the latest added
+         * class mapping.
+         *
+         * @param mapping Pair(originalMethodName, obfuscatedMethodName)
+         */
+        fun addMethodMapping(mapping: Pair<String, String>): Builder {
+            if (currentClassMapping != null) {
+                obfuscatedMethods += Pair(
+                    ClassAndMethod(currentClassMapping!!.second, mapping.second),
+                    ClassAndMethod(currentClassMapping!!.first, mapping.first)
+                )
+            }
+            return this
+        }
+
+        fun build(): ObfuscatedClasses =
+            ObfuscatedClasses(this)
+    }
+}
+
+data class ClassAndMethod(val className: String, val methodName: String)
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/obfuscation/ObfuscationMappingsRecorder.java b/src/resourceshrinker/java/com/android/build/shrinker/obfuscation/ObfuscationMappingsRecorder.java
new file mode 100644
index 0000000..6e76c8d
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/obfuscation/ObfuscationMappingsRecorder.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.build.shrinker.obfuscation;
+
+import com.android.annotations.NonNull;
+import com.android.build.shrinker.ResourceShrinkerModel;
+import java.io.IOException;
+
+/**
+ * Interface for unit that should record obfuscation mappings and contribute it to
+ * ResourceShrinkerModel.obfuscatedClasses.
+ */
+public interface ObfuscationMappingsRecorder {
+
+    /**
+     * Records obfuscation mappings to be able to resolve original class name and original method
+     * name when all references in code are obfuscated. Should create and contribute
+     * ObfuscatedClasses instance to ResourceShrinkerModel.obfuscatedClasses.
+     */
+    void recordObfuscationMappings(@NonNull ResourceShrinkerModel model) throws IOException;
+}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/obfuscation/ProguardMappingsRecorder.kt b/src/resourceshrinker/java/com/android/build/shrinker/obfuscation/ProguardMappingsRecorder.kt
new file mode 100644
index 0000000..dd538c7
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/obfuscation/ProguardMappingsRecorder.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.build.shrinker.obfuscation
+
+import com.android.build.shrinker.ResourceShrinkerModel
+import java.nio.charset.StandardCharsets
+import java.nio.file.Files
+import java.nio.file.Path
+
+/**
+ * Records obfuscation mappings from single file in proguard format.
+ *
+ * @param mappingsFile path to proguard.map file.
+ */
+class ProguardMappingsRecorder(private val mappingsFile: Path) : ObfuscationMappingsRecorder {
+
+    override fun recordObfuscationMappings(model: ResourceShrinkerModel) {
+        model.obfuscatedClasses = extractObfuscatedResourceClasses()
+    }
+
+    internal fun extractObfuscatedResourceClasses(): ObfuscatedClasses {
+        // Proguard obfuscation mappings file has the following structure:
+        // # comment1
+        // com.package.MainClass -> a.a:
+        //     int field -> a
+        //     boolean getFlag() -> b
+        // com.package.R -> a.b:
+        // com.package.R$style -> a.b.a:
+        //     int Toolbar_android_gravity -> i1
+
+        val builder = ObfuscatedClasses.Builder()
+        Files.readAllLines(mappingsFile, StandardCharsets.UTF_8).forEach { line ->
+            when {
+                isMethodMapping(line) -> builder.addMethodMapping(extractMethodMapping(line))
+                isClassMapping(line) -> builder.addClassMapping(extractClassMapping(line))
+            }
+        }
+        return builder.build()
+    }
+
+    private fun isClassMapping(line: String): Boolean = line.contains("->")
+
+    private fun isMethodMapping(line: String): Boolean =
+        (line.startsWith(" ") || line.startsWith("\t")) && line.contains("->")
+
+    private fun extractClassMapping(line: String): Pair<String, String> {
+        val mapping = line.split("->", limit = 2)
+        return Pair(mapping[0].trim(), mapping[1].trim(' ', '\t', ':'))
+    }
+
+    private fun extractMethodMapping(line: String): Pair<String, String> {
+        val mapping = line.split("->", limit = 2)
+        val originalMethod = mapping[0].trim()
+            .substringBeforeLast('(')
+            .substringAfter(' ')
+        val obfuscatedMethod = mapping[1].trim()
+        return Pair(originalMethod, obfuscatedMethod)
+    }
+}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/usages/AppCompat.kt b/src/resourceshrinker/java/com/android/build/shrinker/usages/AppCompat.kt
new file mode 100644
index 0000000..bbfea7c
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/usages/AppCompat.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.build.shrinker.usages
+
+import com.android.build.shrinker.obfuscation.ObfuscatedClasses
+
+internal object AppCompat {
+    // Known AppCompat classes which use Resources.getIdentifier but should not trigger mode that
+    // tries to find used resources based on collected strings.
+    internal val APP_COMPAT_CLASSES_ALLOWED_FOR_GET_IDENTIFIER = setOf(
+        "android.support.v7.widget.SuggestionsAdapter",
+        "android.support.v7.internal.widget.ResourcesWrapper",
+        "android.support.v7.widget.ResourcesWrapper",
+        "android.support.v7.widget.TintContextWrapper\$TintResources"
+    )
+
+    internal fun isAppCompatClass(name: String, obfuscation: ObfuscatedClasses): Boolean =
+        APP_COMPAT_CLASSES_ALLOWED_FOR_GET_IDENTIFIER.contains(obfuscation.resolveOriginalClass(name))
+}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/usages/DexUsageRecorder.kt b/src/resourceshrinker/java/com/android/build/shrinker/usages/DexUsageRecorder.kt
new file mode 100644
index 0000000..3f1a676
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/usages/DexUsageRecorder.kt
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.build.shrinker.usages
+
+import com.android.SdkConstants.DOT_DEX
+import com.android.build.shrinker.ResourceShrinkerModel
+import com.android.build.shrinker.obfuscation.ClassAndMethod
+import com.android.build.shrinker.usages.AppCompat.isAppCompatClass
+import com.android.ide.common.resources.usage.ResourceUsageModel
+import com.android.resources.ResourceType
+import com.android.tools.r8.references.MethodReference
+import java.nio.file.Files
+import java.nio.file.Path
+
+/**
+ * Records resource usages, detects usages of WebViews and {@code Resources#getIdentifier},
+ * gathers string constants from compiled .dex files.
+ *
+ * @param root directory starting from which all .dex files are analyzed.
+ */
+class DexUsageRecorder(val root: Path) : ResourceUsageRecorder {
+
+    override fun recordUsages(model: ResourceShrinkerModel) {
+        // Record resource usages from dex classes. The following cases are covered:
+        // 1. Integer constant which refers to resource id.
+        // 2. Reference to static field in R classes.
+        // 3. Usages of android.content.res.Resources.getIdentifier(...) and
+        //    android.webkit.WebView.load...
+        // 4. All strings which might be used to reference resources by name via
+        //    Resources.getIdentifier.
+
+        Files.walk(root)
+            .filter { Files.isRegularFile(it) }
+            .filter { it.toString().endsWith(DOT_DEX, ignoreCase = true) }
+            .forEach { path ->
+                runResourceShrinkerAnalysis(
+                        Files.readAllBytes(path),
+                        path,
+                        DexFileAnalysisCallback(path, model)
+                )
+            }
+    }
+}
+
+private class DexFileAnalysisCallback(
+        private val path: Path,
+        private val model: ResourceShrinkerModel
+) : AnalysisCallback {
+    companion object {
+        const val ANDROID_RES = "android_res/"
+
+        private fun String.toSourceClassName(): String {
+            return this.replace('/', '.')
+        }
+    }
+
+    // R class methods should only be processed for reachable resource IDs. R class fields that are
+    // not referenced should not be considered since there is no usage in the program.
+    // If the fields have been inlined, the values at the callsite will be recorded when visited.
+    var isRClass: Boolean = false
+
+    // In cases where a value from a method is inlined into a constant, we should still mark the
+    // resource as used.
+    val visitingMethod = MethodVisitingStatus()
+
+    override fun shouldProcess(internalName: String): Boolean {
+        isRClass = isResourceClass(internalName)
+        return true
+    }
+
+    /** Returns whether the given class file name points to an aapt-generated compiled R class. */
+    fun isResourceClass(internalName: String): Boolean {
+        val realClassName =
+            model.obfuscatedClasses.resolveOriginalClass(internalName.toSourceClassName())
+        val lastPart = realClassName.substringAfterLast('.')
+        if (lastPart.startsWith("R$")) {
+            val typeName = lastPart.substring(2)
+            return ResourceType.fromClassName(typeName) != null
+        }
+        return false
+    }
+
+    override fun referencedInt(value: Int) {
+        // Avoid marking R class fields as reachable.
+        if (shouldIgnoreField()) {
+            return
+        }
+        val resource = model.resourceStore.getResource(value)
+        if (ResourceUsageModel.markReachable(resource)) {
+            model.debugReporter.debug {
+                "Marking $resource reachable: referenced from $path"
+            }
+        }
+    }
+
+    override fun referencedStaticField(internalName: String, fieldName: String) {
+        // Avoid marking R class fields as reachable.
+        if (shouldIgnoreField()) {
+            return
+        }
+        val realMethod = model.obfuscatedClasses.resolveOriginalMethod(
+                ClassAndMethod(internalName.toSourceClassName(), fieldName)
+        )
+
+        if (isValidResourceType(realMethod.className)) {
+            val typePart = realMethod.className.substringAfterLast('$')
+            ResourceType.fromClassName(typePart)?.let { type ->
+                model.resourceStore.getResources(type, realMethod.methodName)
+                    .forEach { ResourceUsageModel.markReachable(it) }
+            }
+        }
+    }
+
+    override fun referencedString(value: String) {
+        // Avoid marking R class fields as reachable.
+        if (shouldIgnoreField()) {
+                return
+        }
+        // See if the string is at all eligible; ignore strings that aren't identifiers (has java
+        // identifier chars and nothing but .:/), or are empty or too long.
+        // We also allow "%", used for formatting strings.
+        if (value.isEmpty() || value.length > 80) {
+            return
+        }
+        fun isSpecialCharacter(c: Char) = c == '.' || c == ':' || c == '/' || c == '%'
+
+        if (value.all { Character.isJavaIdentifierPart(it) || isSpecialCharacter(it) } &&
+            value.any { Character.isJavaIdentifierPart(it) }) {
+            model.addStringConstant(value)
+            model.isFoundWebContent = model.isFoundWebContent || value.contains(ANDROID_RES)
+        }
+    }
+
+    override fun referencedMethod(
+            internalName: String,
+            methodName: String,
+            methodDescriptor: String
+    ) {
+        if (isRClass && visitingMethod.isVisiting && visitingMethod.methodName == "<clinit>") {
+            return
+        }
+        if (internalName == "android/content/res/Resources" &&
+            methodName == "getIdentifier" &&
+            methodDescriptor == "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I"
+        ) {
+            // "benign" usages: don't trigger reflection mode just because the user has included
+            // appcompat
+            if (isAppCompatClass(internalName.toSourceClassName(), model.obfuscatedClasses)) {
+                return
+            }
+            model.isFoundGetIdentifier = true
+            // TODO: Check previous instruction and see if we can find a literal String; if so, we
+            // can more accurately dispatch the resource here rather than having to check the whole
+            // string pool!
+        }
+        if (internalName == "android/webkit/WebView" && methodName.startsWith("load")) {
+            model.isFoundWebContent = true
+        }
+    }
+
+    override fun startMethodVisit(methodReference: MethodReference) {
+        visitingMethod.isVisiting = true
+        visitingMethod.methodName = methodReference.methodName
+    }
+
+    override fun endMethodVisit(methodReference: MethodReference) {
+        visitingMethod.isVisiting = false
+        visitingMethod.methodName = null
+    }
+
+    private fun shouldIgnoreField(): Boolean {
+        val visitingFromStaticInitRClass = (isRClass
+                && visitingMethod.isVisiting
+                && (visitingMethod.methodName == "<clinit>"))
+        return visitingFromStaticInitRClass ||
+                isRClass && !visitingMethod.isVisiting
+    }
+
+    private fun isValidResourceType(className: String): Boolean =
+        className.substringAfterLast('.').startsWith("R$")
+}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/usages/ProtoAndroidManifestUsageRecorder.kt b/src/resourceshrinker/java/com/android/build/shrinker/usages/ProtoAndroidManifestUsageRecorder.kt
new file mode 100644
index 0000000..4e6aa3e
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/usages/ProtoAndroidManifestUsageRecorder.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.build.shrinker.usages
+
+import com.android.aapt.Resources.XmlNode
+import com.android.build.shrinker.ResourceShrinkerModel
+import com.android.ide.common.resources.usage.ResourceUsageModel
+import java.nio.file.Files
+import java.nio.file.Path
+
+/**
+ * Records resource usages from AndroidManifest.xml in proto compiled format.
+ *
+ * @param manifest path to AndroidManifest.xml file.
+ */
+class ProtoAndroidManifestUsageRecorder(private val manifest: Path) : ResourceUsageRecorder {
+
+    override fun recordUsages(model: ResourceShrinkerModel) {
+        val root = XmlNode.parseFrom(Files.readAllBytes(manifest))
+        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) }
+    }
+}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/usages/ResourceUsageRecorder.java b/src/resourceshrinker/java/com/android/build/shrinker/usages/ResourceUsageRecorder.java
new file mode 100644
index 0000000..46bfddb
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/usages/ResourceUsageRecorder.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.build.shrinker.usages;
+
+import com.android.annotations.NonNull;
+import com.android.build.shrinker.ResourceShrinkerModel;
+import java.io.IOException;
+
+/**
+ * Interface for unit that should record resource usages and contribute this information to
+ * ResourceShrinkerModel.
+ */
+public interface ResourceUsageRecorder {
+
+  /** Records resource usages. */
+  void recordUsages(@NonNull ResourceShrinkerModel model) throws IOException;
+}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/usages/ToolsAttributeUsageRecorder.kt b/src/resourceshrinker/java/com/android/build/shrinker/usages/ToolsAttributeUsageRecorder.kt
new file mode 100644
index 0000000..3d0dc6f
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/usages/ToolsAttributeUsageRecorder.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.build.shrinker.usages
+
+import com.android.SdkConstants.VALUE_STRICT
+import com.android.build.shrinker.ResourceShrinkerModel
+import com.android.utils.XmlUtils
+import com.google.common.collect.ImmutableMap.copyOf
+import java.io.Reader
+import java.nio.file.Files
+import java.nio.file.Path
+import javax.xml.stream.XMLInputFactory
+
+/**
+ * Records usages of tools:keep, tools:discard and tools:shrinkMode in resources.
+ *
+ * <p>This unit requires to analyze resources in raw XML format because as said in
+ * <a href="https://developer.android.com/studio/write/tool-attributes>documentation</a> these
+ * attributes may appear in any &lt;resources&gt; element and some files that contain such element
+ * are not compiled to proto. For example raw and values resources like res/raw/keep.xml,
+ * res/values/values.xml etc.
+ *
+ * @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)
+            .filter { it.fileName.toString().endsWith(".xml", ignoreCase = true) }
+            .forEach { processRawXml(it, model) }
+    }
+
+    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).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)
+    }
+}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/usages/r8ResourceShrinker.kt b/src/resourceshrinker/java/com/android/build/shrinker/usages/r8ResourceShrinker.kt
new file mode 100644
index 0000000..6bfd90f
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/usages/r8ResourceShrinker.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:JvmName("R8ResourceShrinker")
+
+package com.android.build.shrinker.usages
+
+import com.android.tools.r8.ProgramResource
+import com.android.tools.r8.ProgramResourceProvider
+import com.android.tools.r8.ResourceShrinker
+import com.android.tools.r8.origin.PathOrigin
+import com.android.tools.r8.references.MethodReference
+import java.nio.file.Path
+
+
+fun runResourceShrinkerAnalysis(bytes: ByteArray, file: Path, callback: AnalysisCallback) {
+    val resource =
+        ProgramResource.fromBytes(PathOrigin(file), ProgramResource.Kind.DEX, bytes, null)
+    val provider = ProgramResourceProvider { listOf(resource) }
+
+    val command = ResourceShrinker.Builder().addProgramResourceProvider(provider).build()
+    ResourceShrinker.run(command, AnalysisAdapter(callback))
+}
+
+/** An adapter so R8 API classes do not leak into other modules. */
+class AnalysisAdapter(val impl: AnalysisCallback) : ResourceShrinker.ReferenceChecker {
+    override fun shouldProcess(internalName: String): Boolean = impl.shouldProcess(internalName)
+
+    override fun referencedStaticField(internalName: String, fieldName: String) =
+        impl.referencedStaticField(internalName, fieldName)
+
+    override fun referencedInt(value: Int) = impl.referencedInt(value)
+
+    override fun referencedString(value: String) = impl.referencedString(value)
+
+    override fun referencedMethod(
+        internalName: String, methodName: String, methodDescriptor: String
+    ) = impl.referencedMethod(internalName, methodName, methodDescriptor)
+
+    override fun startMethodVisit(methodReference: MethodReference
+    ) = impl.startMethodVisit(methodReference)
+
+    override fun endMethodVisit(methodReference: MethodReference
+    ) = impl.endMethodVisit(methodReference)
+}
+
+interface AnalysisCallback {
+
+    fun shouldProcess(internalName: String): Boolean
+
+    fun referencedInt(value: Int)
+
+    fun referencedString(value: String)
+
+    fun referencedStaticField(internalName: String, fieldName: String)
+
+    fun referencedMethod(internalName: String, methodName: String, methodDescriptor: String)
+
+    fun startMethodVisit(methodReference: MethodReference)
+
+    fun endMethodVisit(methodReference: MethodReference)
+}
+
+class MethodVisitingStatus(var isVisiting: Boolean = false, var methodName: String? = null)
diff --git a/third_party/r8.tar.gz.sha1 b/third_party/r8.tar.gz.sha1
index 4dd64c4..919e57c 100644
--- a/third_party/r8.tar.gz.sha1
+++ b/third_party/r8.tar.gz.sha1
@@ -1 +1 @@
-1ca30cd6edf6ea17a666947b22a60d2fa5cc8d5f
\ No newline at end of file
+1834c0701c4d967fde7e120aee4b2083a39e535e
\ No newline at end of file