Merge commit 'bb8608b7b0e6059678e2b7dcfcb7b9aba561cfa0' into dev-release
diff --git a/.gitignore b/.gitignore
index 018c71b..a71b445 100644
--- a/.gitignore
+++ b/.gitignore
@@ -165,6 +165,8 @@
 third_party/openjdk/desugar_jdk_libs_releases/1.1.1.tar.gz
 third_party/openjdk/desugar_jdk_libs_releases/1.1.5
 third_party/openjdk/desugar_jdk_libs_releases/1.1.5.tar.gz
+third_party/openjdk/desugar_jdk_libs_releases/2.0.3
+third_party/openjdk/desugar_jdk_libs_releases/2.0.3.tar.gz
 third_party/openjdk/jdk-[0-9][0-9]/linux
 third_party/openjdk/jdk-[0-9][0-9]/linux.tar.gz
 third_party/openjdk/jdk-[0-9][0-9]/osx
@@ -296,8 +298,8 @@
 tools/*/host/art-12.0.0-beta4.tar.gz
 tools/*/host/art-13.0.0
 tools/*/host/art-13.0.0.tar.gz
-tools/*/host/art-14.0.0-dp1
-tools/*/host/art-14.0.0-dp1.tar.gz
+tools/*/host/art-14.0.0-beta3
+tools/*/host/art-14.0.0-beta3.tar.gz
 tools/*/host/art-master
 tools/*/host/art-master.tar.gz
 tools/*/art.tar.gz
diff --git a/build.gradle b/build.gradle
index 04704bc..4b948af 100644
--- a/build.gradle
+++ b/build.gradle
@@ -345,12 +345,12 @@
                 "openjdk/openjdk-rt-1.8",
                 "openjdk/desugar_jdk_libs",
                 "openjdk/desugar_jdk_libs_11",
-                "openjdk/desugar_jdk_libs_legacy",
                 "openjdk/desugar_jdk_libs_releases/1.0.9",
                 "openjdk/desugar_jdk_libs_releases/1.0.10",
                 "openjdk/desugar_jdk_libs_releases/1.1.0",
                 "openjdk/desugar_jdk_libs_releases/1.1.1",
                 "openjdk/desugar_jdk_libs_releases/1.1.5",
+                "openjdk/desugar_jdk_libs_releases/2.0.3",
                 "openjdk/jdk-11-test",
                 "opensource-apps/tivi",
                 "proguard/proguard5.2.1",
@@ -377,7 +377,7 @@
                 "linux/art-10.0.0",
                 "linux/host/art-12.0.0-beta4",
                 "linux/host/art-13.0.0",
-                "linux/host/art-14.0.0-dp1",
+                "linux/host/art-14.0.0-beta3",
                 "linux/host/art-master",
                 "linux/dalvik",
                 "linux/dalvik-4.0.4",
diff --git a/d8_r8/main/build.gradle.kts b/d8_r8/main/build.gradle.kts
index a04d4f1..49e79b5 100644
--- a/d8_r8/main/build.gradle.kts
+++ b/d8_r8/main/build.gradle.kts
@@ -22,6 +22,7 @@
 
 dependencies {
   implementation(":keepanno")
+  implementation(":resourceshrinker")
   compileOnly(Deps.asm)
   compileOnly(Deps.asmCommons)
   compileOnly(Deps.asmUtil)
@@ -37,6 +38,8 @@
   listOf(ThirdPartyDeps.apiDatabase))
 
 val keepAnnoJarTask = projectTask("keepanno", "jar")
+val resourceShrinkerJarTask = projectTask("resourceshrinker", "jar")
+val resourceShrinkerDepsTask = projectTask("resourceshrinker", "depsJar")
 
 fun mainJarDependencies() : FileCollection {
   return sourceSets
@@ -75,9 +78,13 @@
     doFirst {
       println(header("R8 full dependencies"))
     }
+    dependsOn(resourceShrinkerJarTask)
+    dependsOn(resourceShrinkerDepsTask)
     mainJarDependencies().forEach({ println(it) })
     from(mainJarDependencies().map(::zipTree))
     from(keepAnnoJarTask.outputs.files.map(::zipTree))
+    from(resourceShrinkerJarTask.outputs.files.map(::zipTree))
+    from(resourceShrinkerDepsTask.outputs.files.map(::zipTree))
     duplicatesStrategy = DuplicatesStrategy.EXCLUDE
     archiveFileName.set("deps.jar")
   }
diff --git a/d8_r8/main/settings.gradle.kts b/d8_r8/main/settings.gradle.kts
index 765e0f8..019fbf2 100644
--- a/d8_r8/main/settings.gradle.kts
+++ b/d8_r8/main/settings.gradle.kts
@@ -5,4 +5,5 @@
 rootProject.name = "r8"
 
 val root = rootProject.projectDir.parentFile
-includeBuild(root.resolve("keepanno"))
\ No newline at end of file
+includeBuild(root.resolve("keepanno"))
+includeBuild(root.resolve("resourceshrinker"))
diff --git a/d8_r8/r8lib/build.gradle.kts b/d8_r8/r8lib/build.gradle.kts
index c3eea8f..2e4bfa7 100644
--- a/d8_r8/r8lib/build.gradle.kts
+++ b/d8_r8/r8lib/build.gradle.kts
@@ -66,14 +66,17 @@
     dependsOn(r8WithRelocatedDepsTask)
     val r8 = r8WithRelocatedDepsTask.outputs.files.getSingleFile()
     val generatedKeepRules = generateKeepRules.get().outputs.files.getSingleFile()
-    inputs.files(listOf(r8, generatedKeepRules))
+    val keepTxt = getRoot().resolveAll("src", "main", "keep.txt")
+    // TODO(b/294351878): Remove once enum issue is fixed
+    val keepResourceShrinkerTxt = getRoot().resolveAll("src", "main", "keep_r8resourceshrinker.txt")
+    inputs.files(listOf(r8, generatedKeepRules, keepTxt, keepResourceShrinkerTxt))
     val output = file(Paths.get("build", "libs", "r8lib-deps-relocated.jar"))
     outputs.file(output)
     commandLine = createR8LibCommandLine(
       r8,
       r8,
       output,
-      listOf(getRoot().resolveAll("src", "main", "keep.txt"), generatedKeepRules),
+      listOf(keepTxt, generatedKeepRules, keepResourceShrinkerTxt),
       false)
   }
 
@@ -94,4 +97,19 @@
       true,
       listOf(deps))
   }
+
+  val resourceshrinkercli by registering(Exec::class) {
+    dependsOn(r8WithRelocatedDepsTask)
+    val r8 = r8WithRelocatedDepsTask.outputs.files.getSingleFile()
+    val keepTxt = getRoot().resolveAll("src", "main", "resourceshrinker_cli.txt")
+    inputs.file(keepTxt)
+    val output = file(Paths.get("build", "libs", "resourceshrinkercli.jar"))
+    outputs.file(output)
+    commandLine = createR8LibCommandLine(
+      r8,
+      r8,
+      output,
+      listOf(keepTxt),
+      false)
+  }
 }
diff --git a/d8_r8/resourceshrinker/build.gradle.kts b/d8_r8/resourceshrinker/build.gradle.kts
new file mode 100644
index 0000000..e4cda0c
--- /dev/null
+++ b/d8_r8/resourceshrinker/build.gradle.kts
@@ -0,0 +1,60 @@
+// 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))
+    exclude("**/*.proto")
+    exclude("versions-offline/**")
+    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/infra/config/global/generated/cr-buildbucket.cfg b/infra/config/global/generated/cr-buildbucket.cfg
index 5c3f1b9..388f09b 100644
--- a/infra/config/global/generated/cr-buildbucket.cfg
+++ b/infra/config/global/generated/cr-buildbucket.cfg
@@ -142,42 +142,6 @@
       }
     }
     builders {
-      name: "desugared_library-head"
-      swarming_host: "chrome-swarming.appspot.com"
-      swarming_tags: "vpython:native-python-wrapper"
-      dimensions: "cores:8"
-      dimensions: "cpu:x86-64"
-      dimensions: "os:Ubuntu-20.04"
-      dimensions: "pool:luci.r8.ci"
-      exe {
-        cipd_package: "infra_internal/recipe_bundles/chrome-internal.googlesource.com/chrome/tools/build_limited/scripts/slave"
-        cipd_version: "refs/heads/master"
-        cmd: "luciexe"
-      }
-      properties:
-        '{'
-        '  "builder_group": "internal.client.r8",'
-        '  "recipe": "rex",'
-        '  "test_options": ['
-        '    "--one_line_per_test",'
-        '    "--archive_failures",'
-        '    "--no_internal",'
-        '    "--no_arttests",'
-        '    "--desugared-library",'
-        '    "HEAD"'
-        '  ]'
-        '}'
-      priority: 26
-      execution_timeout_secs: 43200
-      expiration_secs: 126000
-      build_numbers: YES
-      service_account: "r8-ci-builder@chops-service-accounts.iam.gserviceaccount.com"
-      experiments {
-        key: "luci.recipes.use_python3"
-        value: 100
-      }
-    }
-    builders {
       name: "desugared_library-jdk11_head"
       swarming_host: "chrome-swarming.appspot.com"
       swarming_tags: "vpython:native-python-wrapper"
diff --git a/infra/config/global/generated/luci-milo.cfg b/infra/config/global/generated/luci-milo.cfg
index b0f2546..8d17a6a 100644
--- a/infra/config/global/generated/luci-milo.cfg
+++ b/infra/config/global/generated/luci-milo.cfg
@@ -151,11 +151,6 @@
     short_name: "jdk8"
   }
   builders {
-    name: "buildbucket/luci.r8.ci/desugared_library-head"
-    category: "library_desugar"
-    short_name: "head"
-  }
-  builders {
     name: "buildbucket/luci.r8.ci/desugared_library-jdk11_head"
     category: "library_desugar"
     short_name: "jdk11_head"
diff --git a/infra/config/global/generated/luci-notify.cfg b/infra/config/global/generated/luci-notify.cfg
index 048dc7a..d0fb763 100644
--- a/infra/config/global/generated/luci-notify.cfg
+++ b/infra/config/global/generated/luci-notify.cfg
@@ -60,18 +60,6 @@
   }
   builders {
     bucket: "ci"
-    name: "desugared_library-head"
-    repository: "https://r8.googlesource.com/r8"
-  }
-}
-notifiers {
-  notifications {
-    on_failure: true
-    on_new_failure: true
-    notify_blamelist {}
-  }
-  builders {
-    bucket: "ci"
     name: "desugared_library-jdk11_head"
     repository: "https://r8.googlesource.com/r8"
   }
diff --git a/infra/config/global/generated/luci-scheduler.cfg b/infra/config/global/generated/luci-scheduler.cfg
index 91a12e2..faedc8a 100644
--- a/infra/config/global/generated/luci-scheduler.cfg
+++ b/infra/config/global/generated/luci-scheduler.cfg
@@ -65,20 +65,6 @@
   }
 }
 job {
-  id: "desugared_library-head"
-  realm: "ci"
-  acl_sets: "ci"
-  triggering_policy {
-    kind: GREEDY_BATCHING
-    max_concurrent_invocations: 4
-  }
-  buildbucket {
-    server: "cr-buildbucket.appspot.com"
-    bucket: "ci"
-    builder: "desugared_library-head"
-  }
-}
-job {
   id: "desugared_library-jdk11_head"
   realm: "ci"
   acl_sets: "ci"
@@ -829,7 +815,6 @@
   triggers: "archive"
   triggers: "cached"
   triggers: "check"
-  triggers: "desugared_library-head"
   triggers: "desugared_library-jdk11_head"
   triggers: "linux-android-10.0.0"
   triggers: "linux-android-12.0.0"
diff --git a/infra/config/global/generated/project.cfg b/infra/config/global/generated/project.cfg
index ee2ef9d..c275232 100644
--- a/infra/config/global/generated/project.cfg
+++ b/infra/config/global/generated/project.cfg
@@ -7,7 +7,7 @@
 name: "r8"
 access: "group:all"
 lucicfg {
-  version: "1.39.8"
+  version: "1.39.11"
   package_dir: ".."
   config_dir: "generated"
   entry_point: "main.star"
diff --git a/infra/config/global/main.star b/infra/config/global/main.star
index dd14d77..520bb77 100755
--- a/infra/config/global/main.star
+++ b/infra/config/global/main.star
@@ -400,30 +400,28 @@
 app_dump()
 
 def desugared_library():
-  for name in ["head", "jdk11_head"]:
-    test_options = [
-        "--one_line_per_test",
-        "--archive_failures",
-        "--no_internal",
-        "--no_arttests",
-        "--desugared-library",
-        "HEAD"
-    ]
-    if "jdk11" in name:
-      test_options = test_options + ["--desugared-library-configuration", "jdk11"]
-    properties = {
-       "builder_group" : "internal.client.r8",
-       "test_options" : test_options,
-    }
-    name = "desugared_library-" + name
-    r8_builder(
-        name,
-        category = "library_desugar",
-        dimensions = get_dimensions(),
-        execution_timeout = time.hour * 12,
-        expiration_timeout = time.hour * 35,
-        properties = properties,
-    )
+  test_options = [
+      "--one_line_per_test",
+      "--archive_failures",
+      "--no_internal",
+      "--no_arttests",
+      "--desugared-library",
+      "HEAD",
+      "--desugared-library-configuration",
+      "jdk11"
+  ]
+  properties = {
+     "builder_group" : "internal.client.r8",
+     "test_options" : test_options,
+  }
+  r8_builder(
+      "desugared_library-jdk11_head",
+      category = "library_desugar",
+      dimensions = get_dimensions(),
+      execution_timeout = time.hour * 12,
+      expiration_timeout = time.hour * 35,
+      properties = properties,
+  )
 desugared_library()
 
 r8_builder(
diff --git a/scripts/add-android-jar.sh b/scripts/add-android-jar.sh
index eabdedc..e6ba2c2 100755
--- a/scripts/add-android-jar.sh
+++ b/scripts/add-android-jar.sh
@@ -16,8 +16,8 @@
 SDK_HOME=$HOME/Android/Sdk
 
 # Modify these to match the SDK android.jar to add.
-SDK_DIR_NAME=android-33
-SDK_VERSION=33
+SDK_DIR_NAME=android-UpsideDownCake
+SDK_VERSION=34
 
 SDK_DIR=$SDK_HOME/platforms/$SDK_DIR_NAME
 THIRD_PARTY_ANDROID_JAR=third_party/android_jar
diff --git a/src/library_desugar/jdk11/desugar_jdk_libs.json b/src/library_desugar/jdk11/desugar_jdk_libs.json
index 05b48f0..965e3cf 100644
--- a/src/library_desugar/jdk11/desugar_jdk_libs.json
+++ b/src/library_desugar/jdk11/desugar_jdk_libs.json
@@ -140,6 +140,7 @@
       "api_level_below_or_equal": 25,
       "rewrite_prefix": {
         "java.time.": "j$.time.",
+        "java.util.Base64": "j$.util.Base64",
         "java.util.Desugar": "j$.util.Desugar"
       },
       "dont_rewrite_prefix": [
@@ -267,6 +268,12 @@
         "java.util.OptionalInt": "java.util.OptionalConversions",
         "java.util.OptionalLong": "java.util.OptionalConversions"
       }
+    },
+    {
+      "api_level_below_or_equal": 18,
+      "rewrite_prefix": {
+        "java.nio.charset.StandardCharsets": "j$.nio.charset.StandardCharsets"
+      }
     }
   ],
   "program_flags": [
@@ -346,6 +353,10 @@
     },
     {
       "api_level_below_or_equal": 25,
+      "rewrite_prefix": {
+        "sun.nio.cs.": "j$.sun.nio.cs.",
+        "sun.util.PreHashedMap": "j$.sun.util.PreHashedMap"
+      },
       "retarget_method": {
         "boolean java.util.Arrays#deepEquals0(java.lang.Object, java.lang.Object)": "java.util.DesugarArrays"
       },
@@ -400,6 +411,14 @@
           "j$.util.stream.Stream": "java.util.stream.Stream"
         }
       }
+    },
+    {
+      "api_level_below_or_equal": 18,
+      "retarget_static_field": {
+        "sun.nio.cs.US_ASCII sun.nio.cs.US_ASCII#INSTANCE": "java.nio.charset.Charset java.nio.charset.StandardCharsets#US_ASCII",
+        "sun.nio.cs.ISO_8859_1 sun.nio.cs.ISO_8859_1#INSTANCE": "java.nio.charset.Charset java.nio.charset.StandardCharsets#ISO_8859_1",
+        "sun.nio.cs.UTF_8 sun.nio.cs.UTF_8#INSTANCE": "java.nio.charset.Charset java.nio.charset.StandardCharsets#UTF_8"
+      }
     }
   ],
   "shrinker_config": [
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 10d9755..b8489e1 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -548,10 +548,7 @@
       appView.setGraphLens(new AppliedGraphLens(appView));
       timing.end();
 
-      if (!options.shouldRerunEnqueuer()) {
-        // TODO(b/225838009): Support tracing and building LIR in Enqueuer.
-        PrimaryR8IRConverter.finalizeLirToOutputFormat(appView, timing, executorService);
-      } else {
+      if (options.shouldRerunEnqueuer()) {
         timing.begin("Post optimization code stripping");
         try {
           GraphConsumer keptGraphConsumer = null;
@@ -656,9 +653,6 @@
           timing.end();
         }
 
-        // TODO(b/225838009): Support LIR in proto shrinking.
-        PrimaryR8IRConverter.finalizeLirToOutputFormat(appView, timing, executorService);
-
         if (appView.options().protoShrinking().isProtoShrinkingEnabled()) {
           if (appView.options().protoShrinking().isEnumLiteProtoShrinkingEnabled()) {
             appView.protoShrinker().enumLiteProtoShrinker.verifyDeadEnumLiteMapsAreDead();
@@ -692,6 +686,9 @@
         }
       }
 
+      // TODO(b/225838009): Check support LIR in bridge remover.
+      PrimaryR8IRConverter.finalizeLirToOutputFormat(appView, timing, executorService);
+
       // Insert a member rebinding oracle in the graph to ensure that all subsequent rewritings of
       // the application has an applied oracle for looking up non-rebound references.
       MemberRebindingIdentityLens memberRebindingIdentityLens =
diff --git a/src/main/java/com/android/tools/r8/graph/DexDebugEventBuilder.java b/src/main/java/com/android/tools/r8/graph/DexDebugEventBuilder.java
index 3b68dc5..399d2b7 100644
--- a/src/main/java/com/android/tools/r8/graph/DexDebugEventBuilder.java
+++ b/src/main/java/com/android/tools/r8/graph/DexDebugEventBuilder.java
@@ -35,6 +35,7 @@
   public static final int NO_PC_INFO = -1;
   private static final int NO_LINE_INFO = -1;
 
+  private final AppView<?> appView;
   private final DexEncodedMethod method;
   private final DexItemFactory factory;
   private final InternalOptions options;
@@ -63,14 +64,15 @@
   // Initial known line for the method.
   private int startLine = NO_LINE_INFO;
 
-  public DexDebugEventBuilder(IRCode code, InternalOptions options) {
+  public DexDebugEventBuilder(AppView<?> appView, IRCode code) {
+    this.appView = appView;
     this.method = code.method();
-    this.factory = options.itemFactory;
-    this.options = options;
+    this.factory = appView.dexItemFactory();
+    this.options = appView.options();
   }
 
   /** Add events at pc for instruction. */
-  public void add(int pc, int postPc, Instruction instruction) {
+  public void add(int pc, int postPc, Instruction instruction, ProgramMethod context) {
     boolean isBlockEntry = instruction.getBlock().entry() == instruction;
     boolean isBlockExit = instruction.getBlock().exit() == instruction;
 
@@ -92,7 +94,7 @@
       updateLocals(instruction.asDebugLocalsChange());
     } else if (pcAdvancing) {
       if (!position.isNone() && !position.equals(emittedPosition)) {
-        if (options.debug || instruction.instructionInstanceCanThrow()) {
+        if (options.debug || instruction.instructionInstanceCanThrow(appView, context)) {
           emitDebugPosition(pc, position);
         }
       }
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index 854ba5b..4b5815c 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -588,6 +588,11 @@
   public final DexType kotlinMetadataType = createStaticallyKnownType(kotlinMetadataDescriptor);
   public final DexType kotlinJvmNameType = createStaticallyKnownType(kotlinJvmNameDescriptor);
 
+  public final DexType kotlinEnumEntriesList =
+      createStaticallyKnownType("Lkotlin/enums/EnumEntriesList;");
+  public final DexMethod kotlinEnumEntriesListInit =
+      createInstanceInitializer(kotlinEnumEntriesList, createArrayType(1, enumType));
+
   public final DexType javaIoFileType = createStaticallyKnownType("Ljava/io/File;");
   public final DexType javaMathBigIntegerType = createStaticallyKnownType("Ljava/math/BigInteger;");
   public final DexType javaNioByteOrderType = createStaticallyKnownType("Ljava/nio/ByteOrder;");
diff --git a/src/main/java/com/android/tools/r8/graph/DexValue.java b/src/main/java/com/android/tools/r8/graph/DexValue.java
index 0dc1d16..b2bc5ce 100644
--- a/src/main/java/com/android/tools/r8/graph/DexValue.java
+++ b/src/main/java/com/android/tools/r8/graph/DexValue.java
@@ -1253,7 +1253,7 @@
       TypeElement type = TypeElement.stringClassType(appView, definitelyNotNull());
       Value outValue = code.createValue(type, local);
       ConstString instruction = new ConstString(outValue, value);
-      if (!instruction.instructionInstanceCanThrow()) {
+      if (!instruction.instructionInstanceCanThrow(appView, code.context())) {
         return instruction;
       }
       return null;
@@ -1346,7 +1346,7 @@
       DexItemBasedConstString instruction =
           new DexItemBasedConstString(outValue, value, nameComputationInfo);
       // DexItemBasedConstString cannot throw.
-      assert !instruction.instructionInstanceCanThrow();
+      assert !instruction.instructionInstanceCanThrow(appView, code.context());
       return instruction;
     }
 
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/constant/SparseConditionalConstantPropagation.java b/src/main/java/com/android/tools/r8/ir/analysis/constant/SparseConditionalConstantPropagation.java
index 1c46b14..de4cf6a 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/constant/SparseConditionalConstantPropagation.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/constant/SparseConditionalConstantPropagation.java
@@ -20,11 +20,10 @@
 import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
 import com.android.tools.r8.ir.optimize.AffectedValues;
 import com.android.tools.r8.utils.BooleanBox;
+import com.android.tools.r8.utils.WorkList;
 import java.util.ArrayList;
 import java.util.BitSet;
-import java.util.Deque;
 import java.util.HashMap;
-import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 
@@ -59,13 +58,9 @@
     private final IRCode code;
     private final Map<Value, LatticeElement> mapping = new HashMap<>();
 
-    // TODO(b/270398965): Replace LinkedList.
-    @SuppressWarnings("JdkObsolete")
-    private final Deque<Value> ssaEdges = new LinkedList<>();
+    private final WorkList<Value> ssaEdges = WorkList.newIdentityWorkList();
 
-    // TODO(b/270398965): Replace LinkedList.
-    @SuppressWarnings("JdkObsolete")
-    private final Deque<BasicBlock> flowEdges = new LinkedList<>();
+    private final WorkList<BasicBlock> flowEdges = WorkList.newIdentityWorkList();
 
     private final BitSet[] executableFlowEdges;
     private final BitSet visitedBlocks;
@@ -81,9 +76,9 @@
       BasicBlock firstBlock = code.entryBlock();
       visitInstructions(firstBlock);
 
-      while (!flowEdges.isEmpty() || !ssaEdges.isEmpty()) {
-        while (!flowEdges.isEmpty()) {
-          BasicBlock block = flowEdges.poll();
+      while (flowEdges.hasNext() || ssaEdges.hasNext()) {
+        while (flowEdges.hasNext()) {
+          BasicBlock block = flowEdges.removeSeen();
           for (Phi phi : block.getPhis()) {
             visitPhi(phi);
           }
@@ -91,8 +86,8 @@
             visitInstructions(block);
           }
         }
-        while (!ssaEdges.isEmpty()) {
-          Value value = ssaEdges.poll();
+        while (ssaEdges.hasNext()) {
+          Value value = ssaEdges.removeSeen();
           for (Phi phi : value.uniquePhiUsers()) {
             visitPhi(phi);
           }
@@ -186,7 +181,7 @@
       if (!element.isTop()) {
         LatticeElement currentPhiElement = getLatticeElement(phi);
         if (currentPhiElement.meet(element) != currentPhiElement) {
-          ssaEdges.add(phi);
+          ssaEdges.addIfNotSeen(phi);
           setLatticeElement(phi, element);
         }
       }
@@ -200,12 +195,12 @@
     }
 
     private void visitInstruction(Instruction instruction) {
-      if (instruction.outValue() != null && !instruction.isDebugLocalUninitialized()) {
+      if (instruction.hasOutValue() && !instruction.isDebugLocalUninitialized()) {
         LatticeElement element = instruction.evaluate(code, this::getLatticeElement);
         LatticeElement currentLattice = getLatticeElement(instruction.outValue());
         if (currentLattice.meet(element) != currentLattice) {
           setLatticeElement(instruction.outValue(), element);
-          ssaEdges.add(instruction.outValue());
+          ssaEdges.addIfNotSeen(instruction.outValue());
         }
       }
       if (instruction.isJumpInstruction()) {
@@ -224,7 +219,7 @@
             BasicBlock target = theIf.targetFromCondition(element.asConst().getConstNumber());
             if (!isExecutableEdge(jumpInstBlockNumber, target.getNumber())) {
               setExecutableEdge(jumpInstBlockNumber, target.getNumber());
-              flowEdges.add(target);
+              flowEdges.addIfNotSeen(target);
             }
             return;
           }
@@ -237,7 +232,7 @@
             BasicBlock target = theIf.targetFromCondition(leftNumber, rightNumber);
             if (!isExecutableEdge(jumpInstBlockNumber, target.getNumber())) {
               setExecutableEdge(jumpInstBlockNumber, target.getNumber());
-              flowEdges.add(target);
+              flowEdges.addIfNotSeen(target);
             }
             return;
           }
@@ -255,7 +250,7 @@
           }
           assert target != null;
           setExecutableEdge(jumpInstBlockNumber, target.getNumber());
-          flowEdges.add(target);
+          flowEdges.addIfNotSeen(target);
           return;
         }
       } else if (jumpInstruction.isStringSwitch()) {
@@ -266,7 +261,7 @@
           assert switchElement.asConst().getConstNumber().isZero();
           BasicBlock target = switchInst.fallthroughBlock();
           setExecutableEdge(jumpInstBlockNumber, target.getNumber());
-          flowEdges.add(target);
+          flowEdges.addIfNotSeen(target);
           return;
         }
       } else {
@@ -276,7 +271,7 @@
       for (BasicBlock dst : jumpInstBlock.getSuccessors()) {
         if (!isExecutableEdge(jumpInstBlockNumber, dst.getNumber())) {
           setExecutableEdge(jumpInstBlockNumber, dst.getNumber());
-          flowEdges.add(dst);
+          flowEdges.addIfNotSeen(dst);
         }
       }
     }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/equivalence/BasicBlockBehavioralSubsumption.java b/src/main/java/com/android/tools/r8/ir/analysis/equivalence/BasicBlockBehavioralSubsumption.java
index d274600..187ac76 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/equivalence/BasicBlockBehavioralSubsumption.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/equivalence/BasicBlockBehavioralSubsumption.java
@@ -223,7 +223,7 @@
   private boolean definesValueWithNonLocalUsages(Instruction instruction) {
     if (instruction.hasOutValue()) {
       Value outValue = instruction.outValue();
-      if (outValue.numberOfPhiUsers() > 0) {
+      if (outValue.hasPhiUsers()) {
         return true;
       }
       for (Instruction user : outValue.uniqueUsers()) {
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedExtensionRegistryShrinker.java b/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedExtensionRegistryShrinker.java
index 5907290..5fbfb59 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedExtensionRegistryShrinker.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedExtensionRegistryShrinker.java
@@ -156,6 +156,7 @@
       }
       IRCodeUtils.removeInstructionAndTransitiveInputsIfNotUsed(code, instruction);
     }
+    code.removeRedundantBlocks();
     assert code.isConsistentSSA(appView);
   }
 
@@ -181,7 +182,7 @@
                 OptimizationFeedbackIgnore.getInstance(),
                 methodProcessor,
                 methodProcessingContext,
-                MethodConversionOptions.forPostLirPhase(appView)),
+                MethodConversionOptions.forLirPhase(appView)),
         executorService);
     timing.end();
   }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteShrinker.java b/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteShrinker.java
index 49b2e91..7caae94 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteShrinker.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteShrinker.java
@@ -257,7 +257,7 @@
                 OptimizationFeedbackIgnore.getInstance(),
                 methodProcessor,
                 methodProcessingContext,
-                MethodConversionOptions.forPostLirPhase(appView)),
+                MethodConversionOptions.forLirPhase(appView)),
         executorService);
     timing.end();
   }
@@ -311,6 +311,7 @@
         assert false;
       }
     }
+    code.removeRedundantBlocks();
     assert code.isConsistentSSA(appView);
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleDexItemBasedStringValue.java b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleDexItemBasedStringValue.java
index bc0aef8..ef1de40 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleDexItemBasedStringValue.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleDexItemBasedStringValue.java
@@ -92,7 +92,7 @@
             debugLocalInfo);
     DexItemBasedConstString instruction =
         new DexItemBasedConstString(returnedValue, item, nameComputationInfo);
-    assert !instruction.instructionInstanceCanThrow();
+    assert !instruction.instructionInstanceCanThrow(appView, context);
     return instruction;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleStringValue.java b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleStringValue.java
index d51ff72..78e547c 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleStringValue.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleStringValue.java
@@ -79,7 +79,7 @@
             stringClassType(appView, definitelyNotNull()),
             debugLocalInfo);
     ConstString instruction = new ConstString(returnedValue, string);
-    assert !instruction.instructionInstanceCanThrow();
+    assert !instruction.instructionInstanceCanThrow(appView, context);
     return instruction;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/code/ArrayAccess.java b/src/main/java/com/android/tools/r8/ir/code/ArrayAccess.java
index eb20267..64305dd 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ArrayAccess.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ArrayAccess.java
@@ -4,6 +4,8 @@
 
 package com.android.tools.r8.ir.code;
 
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.ProgramMethod;
 import java.util.List;
 
 public abstract class ArrayAccess extends Instruction implements ImpreciseMemberTypeInstruction {
@@ -48,7 +50,7 @@
   }
 
   @Override
-  public boolean instructionInstanceCanThrow() {
+  public boolean instructionInstanceCanThrow(AppView<?> appView, ProgramMethod context) {
     // TODO(b/203731608): Add parameters to the method and use abstract value in R8.
     if (index().isConstant() && !array().isPhi() && array().definition.isNewArrayEmpty()) {
       Value newArraySizeValue = array().definition.asNewArrayEmpty().size();
diff --git a/src/main/java/com/android/tools/r8/ir/code/ArrayGet.java b/src/main/java/com/android/tools/r8/ir/code/ArrayGet.java
index ab357ed..836a5a5 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ArrayGet.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ArrayGet.java
@@ -290,11 +290,6 @@
 
   @Override
   public void buildLir(LirBuilder<Value, ?> builder) {
-    if (getMemberType().isObject()) {
-      DexType destType = dest().getType().asReferenceType().toDexType(builder.factory());
-      builder.addArrayGetObject(destType, array(), index());
-    } else {
-      builder.addArrayGetPrimitive(getMemberType(), array(), index());
-    }
+    builder.addArrayGet(getMemberType(), array(), index());
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/ArrayPut.java b/src/main/java/com/android/tools/r8/ir/code/ArrayPut.java
index 483e19d..5daba45 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ArrayPut.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ArrayPut.java
@@ -139,15 +139,9 @@
 
   @Override
   public boolean instructionInstanceCanThrow(AppView<?> appView, ProgramMethod context) {
-    // In debug mode, ArrayPut has a side-effect on the locals.
-    if (appView.options().debug) {
-      return true;
-    }
-
-    // In release mode, ArrayPut has side-effects on the input array, unless the index is in bounds
-    // and the array is never used.
+    // Check that the array is guaranteed to be non-null and that the index is within bounds.
     Value array = array().getAliasedValue();
-    if (array.isPhi() || !array.definition.isNewArrayEmpty()) {
+    if (!array.isDefinedByInstructionSatisfying(Instruction::isNewArrayEmpty)) {
       return true;
     }
 
@@ -168,6 +162,10 @@
       return true;
     }
 
+    if (array.hasLocalInfo() || indexValue.hasLocalInfo() || sizeValue.hasLocalInfo()) {
+      return true;
+    }
+
     // Check for type errors.
     TypeElement arrayType = array.getType();
     TypeElement valueType = value().getType();
@@ -175,10 +173,7 @@
       return true;
     }
     TypeElement memberType = arrayType.asArrayType().getMemberTypeAsValueType();
-    if (!valueType.lessThanOrEqualUpToNullability(memberType, appView)) {
-      return true;
-    }
-    return false;
+    return !valueType.lessThanOrEqualUpToNullability(memberType, appView);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/code/ConstClass.java b/src/main/java/com/android/tools/r8/ir/code/ConstClass.java
index 86c95b3..d9f1db8 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ConstClass.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ConstClass.java
@@ -118,11 +118,6 @@
   }
 
   @Override
-  public boolean instructionInstanceCanThrow() {
-    return true;
-  }
-
-  @Override
   public boolean instructionInstanceCanThrow(AppView<?> appView, ProgramMethod context) {
     DexType baseType = getValue().toBaseType(appView.dexItemFactory());
     if (baseType.isPrimitiveType()) {
diff --git a/src/main/java/com/android/tools/r8/ir/code/ConstString.java b/src/main/java/com/android/tools/r8/ir/code/ConstString.java
index 9c1a421..5106a8b 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ConstString.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ConstString.java
@@ -119,7 +119,6 @@
     return this;
   }
 
-  @Override
   public boolean instructionInstanceCanThrow() {
     // The const-string instruction can be a throwing instruction in DEX, if decode() fails.
     try {
@@ -135,9 +134,15 @@
   }
 
   @Override
+  public boolean instructionInstanceCanThrow(AppView<?> appView, ProgramMethod context) {
+    return instructionInstanceCanThrow();
+  }
+
+  @Override
   public DeadInstructionResult canBeDeadCode(AppView<?> appView, IRCode code) {
     // No side-effect, such as throwing an exception, in CF.
-    if (appView.options().isGeneratingClassFiles() || !instructionInstanceCanThrow()) {
+    if (appView.options().isGeneratingClassFiles()
+        || !instructionInstanceCanThrow(appView, code.context())) {
       return DeadInstructionResult.deadIfOutValueIsDead();
     }
     return DeadInstructionResult.notDead();
@@ -171,7 +176,7 @@
   @Override
   public AbstractValue getAbstractValue(
       AppView<? extends AppInfoWithClassHierarchy> appView, ProgramMethod context) {
-    if (!instructionInstanceCanThrow()) {
+    if (!instructionInstanceCanThrow(appView, context)) {
       return appView.abstractValueFactory().createSingleStringValue(value);
     }
     return UnknownValue.getInstance();
diff --git a/src/main/java/com/android/tools/r8/ir/code/DexItemBasedConstString.java b/src/main/java/com/android/tools/r8/ir/code/DexItemBasedConstString.java
index 51182ac..9b4795e 100644
--- a/src/main/java/com/android/tools/r8/ir/code/DexItemBasedConstString.java
+++ b/src/main/java/com/android/tools/r8/ir/code/DexItemBasedConstString.java
@@ -128,7 +128,7 @@
   }
 
   @Override
-  public boolean instructionInstanceCanThrow() {
+  public boolean instructionInstanceCanThrow(AppView<?> appView, ProgramMethod context) {
     // A const-string instruction can usually throw an exception if the decoding of the string
     // fails. Since this string corresponds to a type or member name, though, decoding cannot fail.
     return false;
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java b/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java
index 468c7bf..8808381 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java
@@ -120,6 +120,10 @@
     return result;
   }
 
+  public boolean mayHaveIf() {
+    return get(Opcodes.IF);
+  }
+
   public boolean mayHaveInitClass() {
     return get(Opcodes.INIT_CLASS);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Instruction.java b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
index 2da97aa..dee8f72 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Instruction.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
@@ -604,17 +604,13 @@
     return false;
   }
 
-  public boolean instructionInstanceCanThrow() {
-    return instructionTypeCanThrow();
-  }
-
   public boolean instructionMayHaveSideEffects(AppView<?> appView, ProgramMethod context) {
     return instructionMayHaveSideEffects(appView, context, SideEffectAssumption.NONE);
   }
 
   public boolean instructionMayHaveSideEffects(
       AppView<?> appView, ProgramMethod context, SideEffectAssumption assumption) {
-    return instructionInstanceCanThrow();
+    return instructionInstanceCanThrow(appView, context);
   }
 
   /**
@@ -625,7 +621,7 @@
       AppView<?> appView, ProgramMethod context);
 
   public boolean instructionInstanceCanThrow(AppView<?> appView, ProgramMethod context) {
-    return instructionInstanceCanThrow();
+    return instructionTypeCanThrow();
   }
 
   /** Returns true is this instruction can be treated as dead code if its outputs are not used. */
diff --git a/src/main/java/com/android/tools/r8/ir/code/NewArrayEmpty.java b/src/main/java/com/android/tools/r8/ir/code/NewArrayEmpty.java
index 477bd14..1d63180 100644
--- a/src/main/java/com/android/tools/r8/ir/code/NewArrayEmpty.java
+++ b/src/main/java/com/android/tools/r8/ir/code/NewArrayEmpty.java
@@ -78,7 +78,7 @@
   }
 
   @Override
-  public boolean instructionInstanceCanThrow() {
+  public boolean instructionInstanceCanThrow(AppView<?> appView, ProgramMethod context) {
     return !(size().definition != null
         && size().definition.isConstNumber()
         && size().definition.asConstNumber().getRawValue() >= 0
@@ -89,7 +89,7 @@
   public AbstractValue getAbstractValue(
       AppView<? extends AppInfoWithClassHierarchy> appView, ProgramMethod context) {
     if (!instructionMayHaveSideEffects(appView, context) && size().getType().isInt()) {
-      assert !instructionInstanceCanThrow();
+      assert !instructionInstanceCanThrow(appView, context);
       return StatefulObjectValue.create(
           appView
               .abstractValueFactory()
@@ -100,7 +100,7 @@
 
   @Override
   public DeadInstructionResult canBeDeadCode(AppView<?> appView, IRCode code) {
-    if (instructionInstanceCanThrow()) {
+    if (instructionInstanceCanThrow(appView, code.context())) {
       return DeadInstructionResult.notDead();
     }
     // This would belong better in instructionInstanceCanThrow, but that is not passed an appInfo.
diff --git a/src/main/java/com/android/tools/r8/ir/code/NewArrayFilledData.java b/src/main/java/com/android/tools/r8/ir/code/NewArrayFilledData.java
index ddf7cba..226bf4f 100644
--- a/src/main/java/com/android/tools/r8/ir/code/NewArrayFilledData.java
+++ b/src/main/java/com/android/tools/r8/ir/code/NewArrayFilledData.java
@@ -133,7 +133,7 @@
   public AbstractValue getAbstractValue(
       AppView<? extends AppInfoWithClassHierarchy> appView, ProgramMethod context) {
     if (!instructionMayHaveSideEffects(appView, context) && size <= Integer.MAX_VALUE) {
-      assert !instructionInstanceCanThrow();
+      assert !instructionInstanceCanThrow(appView, context);
       return StatefulObjectValue.create(
           appView.abstractValueFactory().createKnownLengthArrayState((int) size));
     }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
index 1ddfeac..14b9549 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
@@ -247,7 +247,7 @@
     } while (!ifsNeedingRewrite.isEmpty());
 
     // Build instructions.
-    DexDebugEventBuilder debugEventBuilder = new DexDebugEventBuilder(ir, options);
+    DexDebugEventBuilder debugEventBuilder = new DexDebugEventBuilder(appView, ir);
     List<DexInstruction> dexInstructions = new ArrayList<>(numberOfInstructions);
     int instructionOffset = 0;
     for (com.android.tools.r8.ir.code.Instruction irInstruction : ir.instructions()) {
@@ -260,7 +260,8 @@
         dexInstruction.setOffset(instructionOffset);
         instructionOffset += dexInstruction.getSize();
       }
-      debugEventBuilder.add(instructionStartOffset, instructionOffset, irInstruction);
+      debugEventBuilder.add(
+          instructionStartOffset, instructionOffset, irInstruction, getProgramMethod());
     }
 
     // Workaround dalvik tracing bug, where the dalvik tracing JIT can end up tracing
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/ArrayConstructionSimplifier.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/ArrayConstructionSimplifier.java
index b8a54fb..7ce1b99 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/ArrayConstructionSimplifier.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/ArrayConstructionSimplifier.java
@@ -123,7 +123,7 @@
       }
       FilledArrayConversionInfo info =
           computeConversionInfo(
-              candidate, new LinearFlowInstructionListIterator(code, block, it.nextIndex()));
+              code, candidate, new LinearFlowInstructionListIterator(code, block, it.nextIndex()));
       if (info == null) {
         continue;
       }
@@ -252,7 +252,7 @@
   }
 
   private FilledArrayConversionInfo computeConversionInfo(
-      FilledArrayCandidate candidate, LinearFlowInstructionListIterator it) {
+      IRCode code, FilledArrayCandidate candidate, LinearFlowInstructionListIterator it) {
     NewArrayEmpty newArrayEmpty = candidate.newArrayEmpty;
     assert it.peekPrevious() == newArrayEmpty;
     Value arrayValue = newArrayEmpty.outValue();
@@ -279,7 +279,8 @@
       // optimization so that we do not transform half-initialized arrays into fully initialized
       // arrays on exceptional edges. If the block has no handlers it is not observable so
       // we perform the rewriting.
-      if (block.hasCatchHandlers() && instruction.instructionInstanceCanThrow()) {
+      if (block.hasCatchHandlers()
+          && instruction.instructionInstanceCanThrow(appView, code.context())) {
         return null;
       }
       if (!users.contains(instruction)) {
@@ -299,8 +300,7 @@
       if (!arrayPut.index().isConstNumber()) {
         return null;
       }
-      if (arrayPut.instructionInstanceCanThrow()) {
-        assert false;
+      if (arrayPut.instructionInstanceCanThrow(appView, code.context())) {
         return null;
       }
       int index = arrayPut.index().getConstInstruction().asConstNumber().getIntValue();
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/BranchSimplifier.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/BranchSimplifier.java
index bc9c351..c9df777 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/BranchSimplifier.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/BranchSimplifier.java
@@ -75,7 +75,7 @@
 
   @Override
   protected boolean shouldRewriteCode(IRCode code) {
-    return true;
+    return code.metadata().mayHaveIf() || code.metadata().mayHaveSwitch();
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/NaturalIntLoopRemover.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/NaturalIntLoopRemover.java
index 3bb77d8..5d078fe 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/NaturalIntLoopRemover.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/NaturalIntLoopRemover.java
@@ -61,7 +61,9 @@
   @Override
   protected boolean shouldRewriteCode(IRCode code) {
     // This is relevant only if a loop may be present, which implies at least 4 blocks.
-    return appView.options().enableLoopUnrolling && code.getBlocks().size() >= 4;
+    return appView.options().enableLoopUnrolling
+        && code.metadata().mayHaveIf()
+        && code.getBlocks().size() >= 4;
   }
 
   private boolean isComparisonBlock(BasicBlock comparisonBlockCandidate) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/SplitBranch.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/SplitBranch.java
index 1e1b91c..566996d 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/SplitBranch.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/SplitBranch.java
@@ -40,7 +40,7 @@
   @Override
   protected boolean shouldRewriteCode(IRCode code) {
     // This is relevant only if there is a diamond followed by an if which is a minimum of 6 blocks.
-    return code.getBlocks().size() >= 6;
+    return code.metadata().mayHaveIf() && code.getBlocks().size() >= 6;
   }
 
   /**
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringEventConsumer.java b/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringEventConsumer.java
index 7e03d91..bf7ebd5 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringEventConsumer.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringEventConsumer.java
@@ -21,6 +21,7 @@
 import com.android.tools.r8.ir.desugar.constantdynamic.ConstantDynamicClass;
 import com.android.tools.r8.ir.desugar.constantdynamic.ConstantDynamicDesugaringEventConsumer;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.apiconversion.DesugaredLibraryWrapperSynthesizerEventConsumer.DesugaredLibraryAPIConverterEventConsumer;
+import com.android.tools.r8.ir.desugar.desugaredlibrary.retargeter.DesugaredLibRewriterEventConsumer;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.retargeter.DesugaredLibraryRetargeterSynthesizerEventConsumer.DesugaredLibraryRetargeterInstructionEventConsumer;
 import com.android.tools.r8.ir.desugar.invokespecial.InvokeSpecialBridgeInfo;
 import com.android.tools.r8.ir.desugar.invokespecial.InvokeSpecialToSelfDesugaringEventConsumer;
@@ -66,7 +67,8 @@
         DesugaredLibraryAPIConverterEventConsumer,
         ClasspathEmulatedInterfaceSynthesizerEventConsumer,
         ApiInvokeOutlinerDesugaringEventConsumer,
-        VarHandleDesugaringEventConsumer {
+        VarHandleDesugaringEventConsumer,
+        DesugaredLibRewriterEventConsumer {
 
   public static CfInstructionDesugaringEventConsumer createForD8(
       AppView<?> appView,
@@ -187,6 +189,11 @@
     }
 
     @Override
+    public void acceptDesugaredLibraryBridge(ProgramMethod method, ProgramMethod context) {
+      methodProcessor.scheduleMethodForProcessing(method, outermostEventConsumer);
+    }
+
+    @Override
     public void acceptRecordEqualsHelperMethod(ProgramMethod method, ProgramMethod context) {
       // Intentionally empty. Added to the program using ProgramAdditions.
     }
@@ -527,6 +534,11 @@
     }
 
     @Override
+    public void acceptDesugaredLibraryBridge(ProgramMethod method, ProgramMethod context) {
+      // Intentionally empty. The method will be hit by tracing if required.
+    }
+
+    @Override
     public void acceptRecordEqualsHelperMethod(ProgramMethod method, ProgramMethod context) {
       // Intentionally empty. The method will be hit by tracing if required.
     }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/NonEmptyCfInstructionDesugaringCollection.java b/src/main/java/com/android/tools/r8/ir/desugar/NonEmptyCfInstructionDesugaringCollection.java
index 184dff0..f40acbb 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/NonEmptyCfInstructionDesugaringCollection.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/NonEmptyCfInstructionDesugaringCollection.java
@@ -18,6 +18,7 @@
 import com.android.tools.r8.ir.desugar.constantdynamic.ConstantDynamicInstructionDesugaring;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.apiconversion.DesugaredLibraryAPIConverter;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.disabledesugarer.DesugaredLibraryDisableDesugarer;
+import com.android.tools.r8.ir.desugar.desugaredlibrary.retargeter.DesugaredLibraryLibRewriter;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.retargeter.DesugaredLibraryRetargeter;
 import com.android.tools.r8.ir.desugar.icce.AlwaysThrowingInstructionDesugaring;
 import com.android.tools.r8.ir.desugar.invokespecial.InvokeSpecialToSelfDesugaring;
@@ -84,6 +85,10 @@
     }
     this.nestBasedAccessDesugaring = NestBasedAccessDesugaring.create(appView);
     BackportedMethodRewriter backportedMethodRewriter = new BackportedMethodRewriter(appView);
+    DesugaredLibraryLibRewriter desugaredLibRewriter = DesugaredLibraryLibRewriter.create(appView);
+    if (desugaredLibRewriter != null) {
+      desugarings.add(desugaredLibRewriter);
+    }
     desugaredLibraryRetargeter =
         appView.options().machineDesugaredLibrarySpecification.hasRetargeting()
             ? new DesugaredLibraryRetargeter(appView)
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/retargeter/DesugaredLibRewriterEventConsumer.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/retargeter/DesugaredLibRewriterEventConsumer.java
new file mode 100644
index 0000000..78484e3
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/retargeter/DesugaredLibRewriterEventConsumer.java
@@ -0,0 +1,12 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.desugar.desugaredlibrary.retargeter;
+
+import com.android.tools.r8.graph.ProgramMethod;
+
+public interface DesugaredLibRewriterEventConsumer {
+
+  void acceptDesugaredLibraryBridge(ProgramMethod method, ProgramMethod context);
+}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/retargeter/DesugaredLibraryCfMethods.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/retargeter/DesugaredLibraryCfMethods.java
new file mode 100644
index 0000000..0fdac18
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/retargeter/DesugaredLibraryCfMethods.java
@@ -0,0 +1,232 @@
+// 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.
+
+// ***********************************************************************************
+// GENERATED FILE. DO NOT EDIT! See GenerateDesugaredLibraryBridge.java.
+// ***********************************************************************************
+
+package com.android.tools.r8.ir.desugar.desugaredlibrary.retargeter;
+
+import com.android.tools.r8.cf.code.CfConstString;
+import com.android.tools.r8.cf.code.CfFrame;
+import com.android.tools.r8.cf.code.CfIf;
+import com.android.tools.r8.cf.code.CfInstanceOf;
+import com.android.tools.r8.cf.code.CfInvoke;
+import com.android.tools.r8.cf.code.CfLabel;
+import com.android.tools.r8.cf.code.CfLoad;
+import com.android.tools.r8.cf.code.CfNew;
+import com.android.tools.r8.cf.code.CfReturn;
+import com.android.tools.r8.cf.code.CfStackInstruction;
+import com.android.tools.r8.cf.code.CfStore;
+import com.android.tools.r8.cf.code.CfThrow;
+import com.android.tools.r8.cf.code.CfTryCatch;
+import com.android.tools.r8.cf.code.frame.FrameType;
+import com.android.tools.r8.graph.CfCode;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.ir.code.IfType;
+import com.android.tools.r8.ir.code.ValueType;
+import com.google.common.collect.ImmutableList;
+import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap;
+import java.util.ArrayDeque;
+import java.util.Arrays;
+
+public final class DesugaredLibraryCfMethods {
+
+  public static void registerSynthesizedCodeReferences(DexItemFactory factory) {
+    factory.createSynthesizedType("Landroidx/navigation/NavType$Companion;");
+    factory.createSynthesizedType("Landroidx/navigation/NavType;");
+    factory.createSynthesizedType("Ljava/lang/ClassNotFoundException;");
+    factory.createSynthesizedType("Ljava/lang/RuntimeException;");
+  }
+
+  public static CfCode DesugaredLibraryBridge_fromArgType(
+      DexItemFactory factory, DexMethod method) {
+    CfLabel label0 = new CfLabel();
+    CfLabel label1 = new CfLabel();
+    CfLabel label2 = new CfLabel();
+    CfLabel label3 = new CfLabel();
+    CfLabel label4 = new CfLabel();
+    CfLabel label5 = new CfLabel();
+    CfLabel label6 = new CfLabel();
+    CfLabel label7 = new CfLabel();
+    CfLabel label8 = new CfLabel();
+    return new CfCode(
+        method.holder,
+        4,
+        4,
+        ImmutableList.of(
+            label0,
+            new CfLoad(ValueType.OBJECT, 1),
+            new CfIf(IfType.EQ, ValueType.OBJECT, label1),
+            new CfLoad(ValueType.OBJECT, 1),
+            new CfConstString(factory.createString("java")),
+            new CfInvoke(
+                182,
+                factory.createMethod(
+                    factory.stringType,
+                    factory.createProto(factory.booleanType, factory.stringType),
+                    factory.createString("startsWith")),
+                false),
+            new CfIf(IfType.NE, ValueType.INT, label2),
+            label1,
+            new CfFrame(
+                new Int2ObjectAVLTreeMap<>(
+                    new int[] {0, 1, 2},
+                    new FrameType[] {
+                      FrameType.initializedNonNullReference(
+                          factory.createType("Landroidx/navigation/NavType$Companion;")),
+                      FrameType.initializedNonNullReference(factory.stringType),
+                      FrameType.initializedNonNullReference(factory.stringType)
+                    })),
+            new CfLoad(ValueType.OBJECT, 0),
+            new CfLoad(ValueType.OBJECT, 1),
+            new CfLoad(ValueType.OBJECT, 2),
+            new CfInvoke(
+                182,
+                factory.createMethod(
+                    factory.createType("Landroidx/navigation/NavType$Companion;"),
+                    factory.createProto(
+                        factory.createType("Landroidx/navigation/NavType;"),
+                        factory.stringType,
+                        factory.stringType),
+                    factory.createString("fromArgType")),
+                false),
+            new CfReturn(ValueType.OBJECT),
+            label2,
+            new CfFrame(
+                new Int2ObjectAVLTreeMap<>(
+                    new int[] {0, 1, 2},
+                    new FrameType[] {
+                      FrameType.initializedNonNullReference(
+                          factory.createType("Landroidx/navigation/NavType$Companion;")),
+                      FrameType.initializedNonNullReference(factory.stringType),
+                      FrameType.initializedNonNullReference(factory.stringType)
+                    })),
+            new CfLoad(ValueType.OBJECT, 0),
+            new CfNew(factory.stringBuilderType),
+            new CfStackInstruction(CfStackInstruction.Opcode.Dup),
+            new CfInvoke(
+                183,
+                factory.createMethod(
+                    factory.stringBuilderType,
+                    factory.createProto(factory.voidType),
+                    factory.createString("<init>")),
+                false),
+            new CfConstString(factory.createString("j$")),
+            new CfInvoke(
+                182,
+                factory.createMethod(
+                    factory.stringBuilderType,
+                    factory.createProto(factory.stringBuilderType, factory.stringType),
+                    factory.createString("append")),
+                false),
+            new CfLoad(ValueType.OBJECT, 1),
+            new CfConstString(factory.createString("java")),
+            new CfInvoke(
+                182,
+                factory.createMethod(
+                    factory.stringType,
+                    factory.createProto(factory.intType),
+                    factory.createString("length")),
+                false),
+            new CfInvoke(
+                182,
+                factory.createMethod(
+                    factory.stringType,
+                    factory.createProto(factory.stringType, factory.intType),
+                    factory.createString("substring")),
+                false),
+            new CfInvoke(
+                182,
+                factory.createMethod(
+                    factory.stringBuilderType,
+                    factory.createProto(factory.stringBuilderType, factory.stringType),
+                    factory.createString("append")),
+                false),
+            new CfInvoke(
+                182,
+                factory.createMethod(
+                    factory.stringBuilderType,
+                    factory.createProto(factory.stringType),
+                    factory.createString("toString")),
+                false),
+            new CfLoad(ValueType.OBJECT, 2),
+            new CfInvoke(
+                182,
+                factory.createMethod(
+                    factory.createType("Landroidx/navigation/NavType$Companion;"),
+                    factory.createProto(
+                        factory.createType("Landroidx/navigation/NavType;"),
+                        factory.stringType,
+                        factory.stringType),
+                    factory.createString("fromArgType")),
+                false),
+            label3,
+            new CfReturn(ValueType.OBJECT),
+            label4,
+            new CfFrame(
+                new Int2ObjectAVLTreeMap<>(
+                    new int[] {0, 1, 2},
+                    new FrameType[] {
+                      FrameType.initializedNonNullReference(
+                          factory.createType("Landroidx/navigation/NavType$Companion;")),
+                      FrameType.initializedNonNullReference(factory.stringType),
+                      FrameType.initializedNonNullReference(factory.stringType)
+                    }),
+                new ArrayDeque<>(
+                    Arrays.asList(
+                        FrameType.initializedNonNullReference(
+                            factory.createType("Ljava/lang/RuntimeException;"))))),
+            new CfStore(ValueType.OBJECT, 3),
+            label5,
+            new CfLoad(ValueType.OBJECT, 3),
+            new CfInvoke(
+                182,
+                factory.createMethod(
+                    factory.createType("Ljava/lang/RuntimeException;"),
+                    factory.createProto(factory.throwableType),
+                    factory.createString("getCause")),
+                false),
+            new CfInstanceOf(factory.createType("Ljava/lang/ClassNotFoundException;")),
+            new CfIf(IfType.EQ, ValueType.INT, label7),
+            label6,
+            new CfLoad(ValueType.OBJECT, 0),
+            new CfLoad(ValueType.OBJECT, 1),
+            new CfLoad(ValueType.OBJECT, 2),
+            new CfInvoke(
+                182,
+                factory.createMethod(
+                    factory.createType("Landroidx/navigation/NavType$Companion;"),
+                    factory.createProto(
+                        factory.createType("Landroidx/navigation/NavType;"),
+                        factory.stringType,
+                        factory.stringType),
+                    factory.createString("fromArgType")),
+                false),
+            new CfReturn(ValueType.OBJECT),
+            label7,
+            new CfFrame(
+                new Int2ObjectAVLTreeMap<>(
+                    new int[] {0, 1, 2, 3},
+                    new FrameType[] {
+                      FrameType.initializedNonNullReference(
+                          factory.createType("Landroidx/navigation/NavType$Companion;")),
+                      FrameType.initializedNonNullReference(factory.stringType),
+                      FrameType.initializedNonNullReference(factory.stringType),
+                      FrameType.initializedNonNullReference(
+                          factory.createType("Ljava/lang/RuntimeException;"))
+                    })),
+            new CfLoad(ValueType.OBJECT, 3),
+            new CfThrow(),
+            label8),
+        ImmutableList.of(
+            new CfTryCatch(
+                label2,
+                label3,
+                ImmutableList.of(factory.createType("Ljava/lang/RuntimeException;")),
+                ImmutableList.of(label4))),
+        ImmutableList.of());
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/retargeter/DesugaredLibraryLibRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/retargeter/DesugaredLibraryLibRewriter.java
new file mode 100644
index 0000000..2c039d1
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/retargeter/DesugaredLibraryLibRewriter.java
@@ -0,0 +1,146 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.desugar.desugaredlibrary.retargeter;
+
+import com.android.tools.r8.cf.code.CfInstruction;
+import com.android.tools.r8.cf.code.CfInvoke;
+import com.android.tools.r8.contexts.CompilationContext.MethodProcessingContext;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.CfCode;
+import com.android.tools.r8.graph.DexClassAndMethod;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.MethodAccessFlags;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.desugar.CfInstructionDesugaring;
+import com.android.tools.r8.ir.desugar.CfInstructionDesugaringEventConsumer;
+import com.android.tools.r8.ir.desugar.DesugarDescription;
+import com.google.common.collect.ImmutableMap;
+import java.util.Collections;
+import java.util.Map;
+import java.util.function.BiFunction;
+import org.objectweb.asm.Opcodes;
+
+/**
+ * This holds specific rewritings when using desugared library and specific libraries such as
+ * androidx.
+ */
+public class DesugaredLibraryLibRewriter implements CfInstructionDesugaring {
+
+  private final AppView<?> appView;
+  private final Map<DexMethod, BiFunction<DexItemFactory, DexMethod, CfCode>> rewritings;
+
+  private DesugaredLibraryLibRewriter(
+      AppView<?> appView,
+      Map<DexMethod, BiFunction<DexItemFactory, DexMethod, CfCode>> rewritings) {
+    this.appView = appView;
+    this.rewritings = rewritings;
+  }
+
+  public static DesugaredLibraryLibRewriter create(AppView<?> appView) {
+    if (appView.options().machineDesugaredLibrarySpecification.getRewriteType().isEmpty()) {
+      return null;
+    }
+    Map<DexMethod, BiFunction<DexItemFactory, DexMethod, CfCode>> rewritings = computeMap(appView);
+    if (rewritings.isEmpty()) {
+      return null;
+    }
+    return new DesugaredLibraryLibRewriter(appView, rewritings);
+  }
+
+  public static Map<DexMethod, BiFunction<DexItemFactory, DexMethod, CfCode>> computeMap(
+      AppView<?> appView) {
+    DexItemFactory factory = appView.dexItemFactory();
+    DexType navType = factory.createType("Landroidx/navigation/NavType;");
+    if (!appView.appInfo().hasDefinitionForWithoutExistenceAssert(navType)) {
+      return ImmutableMap.of();
+    }
+    ImmutableMap.Builder<DexMethod, BiFunction<DexItemFactory, DexMethod, CfCode>> builder =
+        ImmutableMap.builder();
+    DexType navTypeCompanion = factory.createType("Landroidx/navigation/NavType$Companion;");
+    DexProto fromProto = factory.createProto(navType, factory.stringType, factory.stringType);
+    DexString name = factory.createString("fromArgType");
+    DexMethod from = factory.createMethod(navTypeCompanion, fromProto, name);
+    DexClassAndMethod dexClassAndMethod = appView.definitionFor(from);
+    if (dexClassAndMethod == null) {
+      appView
+          .options()
+          .reporter
+          .warning(
+              "The class "
+                  + navType
+                  + " is present but not the method "
+                  + from
+                  + " which suggests some unsupported set-up where androidx is pre-shrunk without"
+                  + " keeping the method "
+                  + from
+                  + ".");
+      return ImmutableMap.of();
+    }
+    BiFunction<DexItemFactory, DexMethod, CfCode> cfCodeProvider =
+        DesugaredLibraryCfMethods::DesugaredLibraryBridge_fromArgType;
+    builder.put(from, cfCodeProvider);
+    return builder.build();
+  }
+
+  @Override
+  public DesugarDescription compute(CfInstruction instruction, ProgramMethod context) {
+    if (appView
+        .getSyntheticItems()
+        .isSyntheticOfKind(context.getHolderType(), kinds -> kinds.DESUGARED_LIBRARY_BRIDGE)) {
+      return DesugarDescription.nothing();
+    }
+    if (instruction.isInvoke() && rewritings.containsKey(instruction.asInvoke().getMethod())) {
+      return DesugarDescription.builder()
+          .setDesugarRewrite(
+              (freshLocalProvider,
+                  localStackAllocator,
+                  eventConsumer,
+                  localContext,
+                  methodProcessingContext,
+                  desugarings,
+                  dexItemFactory) -> {
+                DexMethod newInvokeTarget =
+                    ensureBridge(
+                        instruction.asInvoke().getMethod(),
+                        eventConsumer,
+                        methodProcessingContext,
+                        localContext);
+                assert appView.definitionFor(newInvokeTarget.getHolderType()) != null;
+                assert !appView.definitionFor(newInvokeTarget.getHolderType()).isInterface();
+                return Collections.singletonList(
+                    new CfInvoke(Opcodes.INVOKESTATIC, newInvokeTarget, false));
+              })
+          .build();
+    }
+    return DesugarDescription.nothing();
+  }
+
+  private DexMethod ensureBridge(
+      DexMethod source,
+      CfInstructionDesugaringEventConsumer eventConsumer,
+      MethodProcessingContext methodProcessingContext,
+      ProgramMethod localContext) {
+    BiFunction<DexItemFactory, DexMethod, CfCode> target = rewritings.get(source);
+    ProgramMethod newMethod =
+        appView
+            .getSyntheticItems()
+            .createMethod(
+                kinds -> kinds.DESUGARED_LIBRARY_BRIDGE,
+                methodProcessingContext.createUniqueContext(),
+                appView,
+                builder ->
+                    builder
+                        .disableAndroidApiLevelCheck()
+                        .setProto(appView.dexItemFactory().prependHolderToProto(source))
+                        .setAccessFlags(MethodAccessFlags.createPublicStaticSynthetic())
+                        .setCode(methodSig -> target.apply(appView.dexItemFactory(), methodSig)));
+    eventConsumer.acceptDesugaredLibraryBridge(newMethod, localContext);
+    return newMethod.getReference();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/records/RecordFieldValuesRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/records/RecordFieldValuesRewriter.java
index bd8b0fc..394241a 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/records/RecordFieldValuesRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/records/RecordFieldValuesRewriter.java
@@ -72,7 +72,7 @@
                 programMethod,
                 appView,
                 programMethod.getOrigin(),
-                MethodConversionOptions.forPostLirPhase(appView));
+                MethodConversionOptions.forLirPhase(appView));
     boolean done = false;
     ListIterator<BasicBlock> blockIterator = irCode.listIterator();
     while (blockIterator.hasNext()) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
index 892f856..f658311 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
@@ -646,7 +646,8 @@
           } else if (instr.instructionMayHaveSideEffects(appView, code.context())) {
             // If the current instruction is const-string, this could load the parameter name.
             // Just make sure it is indeed not throwing.
-            if (instr.isConstString() && !instr.instructionInstanceCanThrow()) {
+            if (instr.isConstString()
+                && !instr.instructionInstanceCanThrow(appView, code.context())) {
               return InstructionEffect.NO_EFFECT;
             }
             // We found a side effect before a NPE check.
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/BooleanMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/BooleanMethodOptimizer.java
index a4e5008..77121b6 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/BooleanMethodOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/BooleanMethodOptimizer.java
@@ -79,7 +79,7 @@
       Instruction definition = argument.definition;
       if (definition.isConstString()) {
         ConstString constString = definition.asConstString();
-        if (!constString.instructionInstanceCanThrow()) {
+        if (!constString.instructionInstanceCanThrow(appView, code.context())) {
           String value = StringUtils.toLowerCase(constString.getValue().toString());
           if (value.equals("true")) {
             instructionIterator.replaceCurrentInstructionWithConstInt(code, 1);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodSideEffectModelCollection.java b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodSideEffectModelCollection.java
index bc6f506..a579344 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodSideEffectModelCollection.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodSideEffectModelCollection.java
@@ -94,6 +94,8 @@
         .add(dexItemFactory.stringMembers.trim)
         .addAll(dexItemFactory.classMethods.getNames)
         .addAll(dexItemFactory.boxedValueOfMethods())
+        // Required to unbox recent Kotlin enums (See b/268005228).
+        .add(dexItemFactory.kotlinEnumEntriesListInit)
         .build();
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/outliner/OutlinerImpl.java b/src/main/java/com/android/tools/r8/ir/optimize/outliner/OutlinerImpl.java
index eefac9a..55bef08 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/outliner/OutlinerImpl.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/outliner/OutlinerImpl.java
@@ -1259,7 +1259,8 @@
               continue;
             }
             int currentPositionIndex = outlinePositionIndex++;
-            if (current.getPosition() != null && current.instructionInstanceCanThrow()) {
+            if (current.getPosition() != null
+                && current.instructionInstanceCanThrow(appView, method)) {
               positionBuilder.addOutlinePosition(currentPositionIndex, current.getPosition());
             }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java
index 202dc62..fb50e54 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java
@@ -188,7 +188,7 @@
       Value rcv = invoke.getReceiver().getAliasedValue();
       if (rcv.definition == null
           || !rcv.definition.isConstString()
-          || rcv.definition.asConstString().instructionInstanceCanThrow()
+          || rcv.definition.asConstString().instructionInstanceCanThrow(appView, code.context())
           || rcv.hasLocalInfo()) {
         continue;
       }
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataRewriter.java b/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataRewriter.java
index 04392f3..583bd40 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataRewriter.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataRewriter.java
@@ -89,17 +89,21 @@
     GraphLens kotlinMetadataLens = appView.getKotlinMetadataLens();
     DexType rewrittenMetadataType =
         graphLens.lookupClassType(factory.kotlinMetadataType, kotlinMetadataLens);
-    DexClass kotlinMetadata = appView.definitionFor(rewrittenMetadataType);
+    // The Kotlin metadata may be present in the input but pruned away in the final tree shaking.
+    DexClass kotlinMetadata =
+        appView.appInfo().definitionForWithoutExistenceAssert(rewrittenMetadataType);
     WriteMetadataFieldInfo writeMetadataFieldInfo =
-        new WriteMetadataFieldInfo(
-            kotlinMetadataFieldExists(kotlinMetadata, appView, kotlin.metadata.kind),
-            kotlinMetadataFieldExists(kotlinMetadata, appView, kotlin.metadata.metadataVersion),
-            kotlinMetadataFieldExists(kotlinMetadata, appView, kotlin.metadata.bytecodeVersion),
-            kotlinMetadataFieldExists(kotlinMetadata, appView, kotlin.metadata.data1),
-            kotlinMetadataFieldExists(kotlinMetadata, appView, kotlin.metadata.data2),
-            kotlinMetadataFieldExists(kotlinMetadata, appView, kotlin.metadata.extraString),
-            kotlinMetadataFieldExists(kotlinMetadata, appView, kotlin.metadata.packageName),
-            kotlinMetadataFieldExists(kotlinMetadata, appView, kotlin.metadata.extraInt));
+        kotlinMetadata == null
+            ? WriteMetadataFieldInfo.rewriteAll()
+            : new WriteMetadataFieldInfo(
+                kotlinMetadataFieldExists(kotlinMetadata, appView, kotlin.metadata.kind),
+                kotlinMetadataFieldExists(kotlinMetadata, appView, kotlin.metadata.metadataVersion),
+                kotlinMetadataFieldExists(kotlinMetadata, appView, kotlin.metadata.bytecodeVersion),
+                kotlinMetadataFieldExists(kotlinMetadata, appView, kotlin.metadata.data1),
+                kotlinMetadataFieldExists(kotlinMetadata, appView, kotlin.metadata.data2),
+                kotlinMetadataFieldExists(kotlinMetadata, appView, kotlin.metadata.extraString),
+                kotlinMetadataFieldExists(kotlinMetadata, appView, kotlin.metadata.packageName),
+                kotlinMetadataFieldExists(kotlinMetadata, appView, kotlin.metadata.extraInt));
     ThreadUtils.processItems(
         appView.appInfo().classes(),
         clazz -> {
diff --git a/src/main/java/com/android/tools/r8/lightir/IR2LirConverter.java b/src/main/java/com/android/tools/r8/lightir/IR2LirConverter.java
index 7f3335f..2e191a7 100644
--- a/src/main/java/com/android/tools/r8/lightir/IR2LirConverter.java
+++ b/src/main/java/com/android/tools/r8/lightir/IR2LirConverter.java
@@ -133,7 +133,7 @@
       Value[] operands = new Value[block.getPredecessors().size()];
       for (Phi phi : block.getPhis()) {
         permuteOperands(phi.getOperands(), permutation, operands);
-        builder.addPhi(phi.getType(), Arrays.asList(operands));
+        builder.addPhi(Arrays.asList(operands));
         valuesOffset++;
       }
     }
diff --git a/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java b/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java
index 16a09d6..a8cb780 100644
--- a/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java
+++ b/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java
@@ -927,7 +927,7 @@
     }
 
     @Override
-    public void onPhi(DexType type, List<EV> operands) {
+    public void onPhi(List<EV> operands) {
       // The type of the phi is determined by its operands during type widening.
       Phi phi = getPhiForNextInstructionAndAdvanceState(TypeElement.getBottom());
       List<Value> values = new ArrayList<>(operands.size());
@@ -1023,19 +1023,18 @@
     }
 
     @Override
-    public void onArrayGetObject(DexType unusedType, EV array, EV index) {
-      // TODO(b/225838009): Remove type and unify object/primitive methods now that it is computed.
-      // The output type depends on its input array member type, so it is computed by widening.
-      Value dest = getOutValueForNextInstruction(TypeElement.getBottom());
-      addInstruction(new ArrayGet(MemberType.OBJECT, dest, getValue(array), getValue(index)));
-    }
-
-    @Override
-    public void onArrayGetPrimitive(MemberType type, EV array, EV index) {
-      // Convert the member type to a "stack value type", e.g., byte, char etc to int.
-      ValueType valueType = ValueType.fromMemberType(type);
-      DexType dexType = valueType.toDexType(appView.dexItemFactory());
-      Value dest = getOutValueForNextInstruction(dexType.toTypeElement(appView));
+    public void onArrayGet(MemberType type, EV array, EV index) {
+      TypeElement typeElement;
+      if (type.isObject()) {
+        // The actual object type must be computed from its array value.
+        typeElement = TypeElement.getBottom();
+      } else {
+        // Convert the member type to a "stack value type", e.g., byte, char etc to int.
+        ValueType valueType = ValueType.fromMemberType(type);
+        DexType dexType = valueType.toDexType(appView.dexItemFactory());
+        typeElement = dexType.toTypeElement(appView);
+      }
+      Value dest = getOutValueForNextInstruction(typeElement);
       addInstruction(new ArrayGet(type, dest, getValue(array), getValue(index)));
     }
 
diff --git a/src/main/java/com/android/tools/r8/lightir/LirBuilder.java b/src/main/java/com/android/tools/r8/lightir/LirBuilder.java
index 0cd52a4..b450066 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirBuilder.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirBuilder.java
@@ -773,9 +773,8 @@
     return addOneItemInstruction(LirOpcodes.MOVEEXCEPTION, exceptionType);
   }
 
-  public LirBuilder<V, EV> addPhi(TypeElement type, List<V> operands) {
-    DexType dexType = toDexType(type);
-    return addInstructionTemplate(LirOpcodes.PHI, Collections.singletonList(dexType), operands);
+  public LirBuilder<V, EV> addPhi(List<V> operands) {
+    return addInstructionTemplate(LirOpcodes.PHI, Collections.emptyList(), operands);
   }
 
   public LirBuilder<V, EV> addDebugLocalWrite(V src) {
@@ -881,14 +880,12 @@
     return addOneValueInstruction(opcode, value);
   }
 
-  public LirBuilder<V, EV> addArrayGetObject(DexType destType, V array, V index) {
-    return addInstructionTemplate(
-        LirOpcodes.AALOAD, Collections.singletonList(destType), ImmutableList.of(array, index));
-  }
-
-  public LirBuilder<V, EV> addArrayGetPrimitive(MemberType memberType, V array, V index) {
+  public LirBuilder<V, EV> addArrayGet(MemberType memberType, V array, V index) {
     int opcode;
     switch (memberType) {
+      case OBJECT:
+        opcode = LirOpcodes.AALOAD;
+        break;
       case BOOLEAN_OR_BYTE:
         opcode = LirOpcodes.BALOAD;
         break;
diff --git a/src/main/java/com/android/tools/r8/lightir/LirParsedInstructionCallback.java b/src/main/java/com/android/tools/r8/lightir/LirParsedInstructionCallback.java
index e3ca61d..5f1411d 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirParsedInstructionCallback.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirParsedInstructionCallback.java
@@ -113,23 +113,12 @@
   }
 
   private void onArrayGetInternal(MemberType type, LirInstructionView view) {
-    if (type.isObject()) {
-      DexType destType = (DexType) getConstantItem(view.getNextConstantOperand());
-      EV array = getNextValueOperand(view);
-      EV index = getNextValueOperand(view);
-      onArrayGetObject(destType, array, index);
-    } else {
-      EV array = getNextValueOperand(view);
-      EV index = getNextValueOperand(view);
-      onArrayGetPrimitive(type, array, index);
-    }
+    EV array = getNextValueOperand(view);
+    EV index = getNextValueOperand(view);
+    onArrayGet(type, array, index);
   }
 
-  public void onArrayGetPrimitive(MemberType type, EV array, EV index) {
-    onInstruction();
-  }
-
-  public void onArrayGetObject(DexType type, EV array, EV index) {
+  public void onArrayGet(MemberType type, EV array, EV index) {
     onInstruction();
   }
 
@@ -497,7 +486,7 @@
     onInstruction();
   }
 
-  public void onPhi(DexType type, List<EV> operands) {
+  public void onPhi(List<EV> operands) {
     onInstruction();
   }
 
@@ -1176,12 +1165,11 @@
         }
       case LirOpcodes.PHI:
         {
-          DexType type = getNextDexTypeOperand(view);
           List<EV> operands = new ArrayList<>();
           while (view.hasMoreOperands()) {
             operands.add(getNextValueOperand(view));
           }
-          onPhi(type, operands);
+          onPhi(operands);
           return;
         }
       case LirOpcodes.FALLTHROUGH:
diff --git a/src/main/java/com/android/tools/r8/lightir/LirPrinter.java b/src/main/java/com/android/tools/r8/lightir/LirPrinter.java
index c3a8406..f2fcbac 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirPrinter.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirPrinter.java
@@ -385,14 +385,7 @@
   }
 
   @Override
-  public void onArrayGetPrimitive(MemberType type, EV array, EV index) {
-    appendOutValue();
-    appendValueArguments(array, index);
-    builder.append(type);
-  }
-
-  @Override
-  public void onArrayGetObject(DexType type, EV array, EV index) {
+  public void onArrayGet(MemberType type, EV array, EV index) {
     appendOutValue();
     appendValueArguments(array, index);
     builder.append(type);
@@ -410,10 +403,9 @@
   }
 
   @Override
-  public void onPhi(DexType type, List<EV> operands) {
+  public void onPhi(List<EV> operands) {
     appendOutValue();
     appendValueArguments(operands);
-    builder.append(type);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/lightir/LirUseRegistryCallback.java b/src/main/java/com/android/tools/r8/lightir/LirUseRegistryCallback.java
index 3b43c56..00e00ca 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirUseRegistryCallback.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirUseRegistryCallback.java
@@ -87,7 +87,7 @@
   }
 
   @Override
-  public void onPhi(DexType type, List<EV> operands) {
+  public void onPhi(List<EV> operands) {
     // Nothing to register.
   }
 
diff --git a/src/main/java/com/android/tools/r8/profile/rewriting/ProfileRewritingCfInstructionDesugaringEventConsumer.java b/src/main/java/com/android/tools/r8/profile/rewriting/ProfileRewritingCfInstructionDesugaringEventConsumer.java
index 94232e2..3050741 100644
--- a/src/main/java/com/android/tools/r8/profile/rewriting/ProfileRewritingCfInstructionDesugaringEventConsumer.java
+++ b/src/main/java/com/android/tools/r8/profile/rewriting/ProfileRewritingCfInstructionDesugaringEventConsumer.java
@@ -427,4 +427,10 @@
     assert parent.verifyNothingToFinalize();
     return true;
   }
+
+  @Override
+  public void acceptDesugaredLibraryBridge(ProgramMethod method, ProgramMethod context) {
+    additionsCollection.addMethodAndHolderIfContextIsInProfile(method, context);
+    parent.acceptDesugaredLibraryBridge(method, context);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
index 4b80427..4707606 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
@@ -98,6 +98,8 @@
       generator.forSingleMethodWithGlobalMerging("ApiModelOutline");
   public final SyntheticKind API_MODEL_OUTLINE_WITHOUT_GLOBAL_MERGING =
       generator.forSingleMethod("ApiModelOutline");
+  public final SyntheticKind DESUGARED_LIBRARY_BRIDGE =
+      generator.forSingleMethod("DesugaredLibraryBridge");
 
   private final List<SyntheticKind> ALL_KINDS;
   private String lazyVersionHash = null;
diff --git a/src/main/java/com/android/tools/r8/utils/DeterminismChecker.java b/src/main/java/com/android/tools/r8/utils/DeterminismChecker.java
index c1817a7..e5c2c66 100644
--- a/src/main/java/com/android/tools/r8/utils/DeterminismChecker.java
+++ b/src/main/java/com/android/tools/r8/utils/DeterminismChecker.java
@@ -153,12 +153,16 @@
     public boolean onLine(String unescapedLine) throws IOException {
       String line = escape(unescapedLine);
       String dumpLine = reader.readLine();
-      boolean equals = dumpLine.equals(line);
-      if (!equals) {
-        throw new AssertionError(
-            "\nMismatch for line: " + line + "\n" + "    and dump-line: " + dumpLine);
+      if (!dumpLine.equals(line)) {
+        // The line might contain a non unicode points. If so, the dump line will have mapped those
+        // to ? when writing. Decode the string and retry equals.
+        String decoded = new String(line.getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8);
+        if (!decoded.equals(dumpLine)) {
+          throw new AssertionError(
+              "\nMismatch for line: " + decoded + "\n" + "    and dump-line: " + dumpLine);
+        }
       }
-      return equals;
+      return true;
     }
 
     @Override
diff --git a/src/main/keep_r8resourceshrinker.txt b/src/main/keep_r8resourceshrinker.txt
new file mode 100644
index 0000000..72bc436
--- /dev/null
+++ b/src/main/keep_r8resourceshrinker.txt
@@ -0,0 +1,6 @@
+# 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.
+
+-keepclassmembers enum com.android.resources.ResourceType { public static **[] values(); }
+-keepclassmembers enum com.android.resources.ResourceFolderType { public static **[] values(); }
diff --git a/src/main/resourceshrinker_cli.txt b/src/main/resourceshrinker_cli.txt
new file mode 100644
index 0000000..6273a47
--- /dev/null
+++ b/src/main/resourceshrinker_cli.txt
@@ -0,0 +1,10 @@
+# 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.
+
+-keepclassmembers enum com.android.resources.ResourceType { public static **[] values(); }
+-keepclassmembers enum com.android.resources.ResourceFolderType { public static **[] values(); }
+
+-keep class com.android.build.shrinker.ResourceShrinkerCli {
+    public static void main(java.lang.String[]);
+}
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..3a3e3a6
--- /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..b96712c
--- /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..17ce9cf
--- /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..21feb52
--- /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..e32f8f6
--- /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/src/test/examples/shaking1/print-mapping-dex.ref b/src/test/examples/shaking1/print-mapping-dex.ref
index bb72965..cd0d3a3 100644
--- a/src/test/examples/shaking1/print-mapping-dex.ref
+++ b/src/test/examples/shaking1/print-mapping-dex.ref
@@ -1,9 +1,8 @@
 shaking1.Shaking -> shaking1.Shaking:
+    0:8:void main(java.lang.String[]):8:8 -> main
+    9:21:void main(java.lang.String[]):9:9 -> main
 shaking1.Used -> a.a:
-    0:2:void <init>(java.lang.String):12:12 -> <init>
-    0:2:java.lang.String aMethodThatIsNotUsedButKept():0:0 -> aMethodThatIsNotUsedButKept
-    3:5:void <init>(java.lang.String):13:13 -> <init>
-    0:6:void main(java.lang.String[]):8:8 -> main
-    7:21:void main(java.lang.String[]):9:9 -> main
-    0:16:java.lang.String method():17:17 -> a
     java.lang.String name -> a
+    0:5:void <init>(java.lang.String):12:12 -> <init>
+    0:16:java.lang.String method():17:17 -> a
+    0:2:java.lang.String aMethodThatIsNotUsedButKept():0:0 -> aMethodThatIsNotUsedButKept
diff --git a/src/test/examplesJava17/records/RecordWithAnnotations.java b/src/test/examplesJava17/records/RecordWithAnnotations.java
index d1648b6..fe9fd95 100644
--- a/src/test/examplesJava17/records/RecordWithAnnotations.java
+++ b/src/test/examplesJava17/records/RecordWithAnnotations.java
@@ -10,6 +10,9 @@
 import java.lang.annotation.Target;
 import java.lang.reflect.Field;
 import java.lang.reflect.RecordComponent;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
 
 public class RecordWithAnnotations {
 
@@ -56,16 +59,39 @@
         System.out.println(c.getType().getName());
         System.out.println(c.getGenericSignature() == null);
         System.out.println(c.getAnnotations().length);
+        // Collect and sort the annotations, as the order is not deterministic on Art (tested
+        // on Art 14 Beta 3).
+        List<String> annotations = new ArrayList<>();
         for (int j = 0; j < c.getAnnotations().length; j++) {
-          System.out.println(c.getAnnotations()[j]);
+          annotations.add(c.getAnnotations()[j].toString());
+        }
+        annotations.sort(Comparator.naturalOrder());
+        for (int j = 0; j < annotations.size(); j++) {
+          System.out.println(annotations.get(j));
         }
       }
       System.out.println(Person.class.getDeclaredFields().length);
+      List<Field> fields = new ArrayList<>();
       for (int i = 0; i < Person.class.getDeclaredFields().length; i++) {
-        Field f = Person.class.getDeclaredFields()[i];
+        fields.add(Person.class.getDeclaredFields()[i]);
+      }
+      fields.sort(
+          new Comparator<Field>() {
+            @Override
+            public int compare(Field o1, Field o2) {
+              return o1.getName().compareTo(o2.getName());
+            }
+          });
+      for (int i = 0; i < fields.size(); i++) {
+        Field f = fields.get(i);
         System.out.println(f.getDeclaredAnnotations().length);
+        List<String> annotations = new ArrayList<>();
         for (int j = 0; j < f.getDeclaredAnnotations().length; j++) {
-          System.out.println(f.getAnnotations()[j]);
+          annotations.add(f.getAnnotations()[j].toString());
+        }
+        annotations.sort(Comparator.naturalOrder());
+        for (int j = 0; j < annotations.size(); j++) {
+          System.out.println(annotations.get(j));
         }
       }
     }
diff --git a/src/test/java/com/android/tools/r8/TestRunResult.java b/src/test/java/com/android/tools/r8/TestRunResult.java
index 28625eb..595b3b7 100644
--- a/src/test/java/com/android/tools/r8/TestRunResult.java
+++ b/src/test/java/com/android/tools/r8/TestRunResult.java
@@ -168,6 +168,11 @@
     return assertFailure();
   }
 
+  public RR assertFailureWithOutputThatMatches(Matcher<String> matcher) {
+    assertStdoutMatches(matcher);
+    return assertFailure();
+  }
+
   public RR assertFailureWithErrorThatThrows(Class<? extends Throwable> expectedError) {
     return assertFailureWithErrorThatMatches(containsString(expectedError.getName()));
   }
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index 1151357..596a13d 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -789,7 +789,7 @@
       ImmutableMap.<DexVm, String>builder()
           .put(DexVm.ART_DEFAULT, "art")
           .put(DexVm.ART_MASTER_HOST, "host/art-master")
-          .put(DexVm.ART_14_0_0_HOST, "host/art-14.0.0-dp1")
+          .put(DexVm.ART_14_0_0_HOST, "host/art-14.0.0-beta3")
           .put(DexVm.ART_13_0_0_HOST, "host/art-13.0.0")
           .put(DexVm.ART_12_0_0_HOST, "host/art-12.0.0-beta4")
           .put(DexVm.ART_10_0_0_HOST, "art-10.0.0")
diff --git a/src/test/java/com/android/tools/r8/apimodel/AndroidApiHashingDatabaseBuilderGeneratorTest.java b/src/test/java/com/android/tools/r8/apimodel/AndroidApiHashingDatabaseBuilderGeneratorTest.java
index cda9c15..8605db0 100644
--- a/src/test/java/com/android/tools/r8/apimodel/AndroidApiHashingDatabaseBuilderGeneratorTest.java
+++ b/src/test/java/com/android/tools/r8/apimodel/AndroidApiHashingDatabaseBuilderGeneratorTest.java
@@ -110,9 +110,9 @@
                   methodReferences.forEach(field -> numberOfMethods.increment())));
         });
     // These numbers will change when updating api-versions.xml
-    assertEquals(5635, parsedApiClasses.size());
-    assertEquals(29017, numberOfFields.get());
-    assertEquals(44107, numberOfMethods.get());
+    assertEquals(5716, parsedApiClasses.size());
+    assertEquals(29609, numberOfFields.get());
+    assertEquals(44827, numberOfMethods.get());
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/benchmarks/desugaredlib/L8Benchmark.java b/src/test/java/com/android/tools/r8/benchmarks/desugaredlib/L8Benchmark.java
index 9095f727..3f12fc8 100644
--- a/src/test/java/com/android/tools/r8/benchmarks/desugaredlib/L8Benchmark.java
+++ b/src/test/java/com/android/tools/r8/benchmarks/desugaredlib/L8Benchmark.java
@@ -3,23 +3,19 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.benchmarks.desugaredlib;
 
-import static com.android.tools.r8.ToolHelper.getDesugarLibConversions;
-import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.CustomConversionVersion.LATEST;
-
 import com.android.tools.r8.L8TestBuilder;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestState;
+import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.benchmarks.BenchmarkBase;
 import com.android.tools.r8.benchmarks.BenchmarkConfig;
 import com.android.tools.r8.benchmarks.BenchmarkDependency;
 import com.android.tools.r8.benchmarks.BenchmarkEnvironment;
 import com.android.tools.r8.benchmarks.BenchmarkTarget;
-import com.android.tools.r8.desugar.desugaredlibrary.jdk11.DesugaredLibraryJDK11Undesugarer;
 import com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
-import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.List;
 import org.junit.runner.RunWith;
@@ -29,10 +25,12 @@
 @RunWith(Parameterized.class)
 public class L8Benchmark extends BenchmarkBase {
 
-  private static final BenchmarkDependency androidJar = BenchmarkDependency.getAndroidJar30();
-  private static final BenchmarkDependency jdk11Conf =
+  private static final BenchmarkDependency ANDROID_JAR = BenchmarkDependency.getAndroidJar30();
+  private static final BenchmarkDependency LEGACY_CONF =
       new BenchmarkDependency(
-          "legacyConf", "desugar_jdk_libs_11", Paths.get("third_party", "openjdk"));
+          "legacyConf",
+          "2.0.3",
+          Paths.get(ToolHelper.THIRD_PARTY_DIR, "openjdk", "desugar_jdk_libs_releases"));
 
   public L8Benchmark(BenchmarkConfig config, TestParameters parameters) {
     super(config, parameters);
@@ -50,23 +48,21 @@
             .setTarget(BenchmarkTarget.D8)
             .setFromRevision(12733)
             .setMethod(L8Benchmark::run)
-            .addDependency(androidJar)
-            .addDependency(jdk11Conf)
+            .addDependency(ANDROID_JAR)
+            .addDependency(LEGACY_CONF)
             .measureRunTime()
             .build());
   }
 
   public static void run(BenchmarkEnvironment environment) throws Exception {
-    Path undesugarJdkLib =
-        DesugaredLibraryJDK11Undesugarer.undesugaredJarJDK11(
-            environment.getTemp().newFolder("undesugar_jdk_lib").toPath(),
-            jdk11Conf.getRoot(environment).resolve("desugar_jdk_libs.jar"));
     LibraryDesugaringSpecification spec =
         new LibraryDesugaringSpecification(
             "JDK11_Benchmark",
-            ImmutableSet.of(undesugarJdkLib, getDesugarLibConversions(LATEST)),
-            Paths.get("src/library_desugar/jdk11/desugar_jdk_libs.json"),
-            ImmutableSet.of(androidJar.getRoot(environment).resolve("android.jar")),
+            ImmutableSet.of(
+                LEGACY_CONF.getRoot(environment).resolve("desugar_jdk_libs.jar"),
+                LEGACY_CONF.getRoot(environment).resolve("desugar_jdk_libs_configuration.jar")),
+            LEGACY_CONF.getRoot(environment).resolve("desugar.json"),
+            ImmutableSet.of(ANDROID_JAR.getRoot(environment).resolve("android.jar")),
             LibraryDesugaringSpecification.JDK11_DESCRIPTOR,
             "");
     runner(environment.getConfig())
diff --git a/src/test/java/com/android/tools/r8/benchmarks/desugaredlib/LegacyDesugaredLibraryBenchmark.java b/src/test/java/com/android/tools/r8/benchmarks/desugaredlib/LegacyDesugaredLibraryBenchmark.java
index 0e86477..6b5e4f9 100644
--- a/src/test/java/com/android/tools/r8/benchmarks/desugaredlib/LegacyDesugaredLibraryBenchmark.java
+++ b/src/test/java/com/android/tools/r8/benchmarks/desugaredlib/LegacyDesugaredLibraryBenchmark.java
@@ -24,12 +24,12 @@
 @RunWith(Parameterized.class)
 public class LegacyDesugaredLibraryBenchmark extends BenchmarkBase {
 
-  private static final BenchmarkDependency androidJar = BenchmarkDependency.getAndroidJar30();
-  private static final BenchmarkDependency legacyConf =
+  private static final BenchmarkDependency ANDROID_JAR = BenchmarkDependency.getAndroidJar30();
+  private static final BenchmarkDependency LEGACY_CONF =
       new BenchmarkDependency(
           "legacyConf",
-          "desugar_jdk_libs_legacy",
-          Paths.get(ToolHelper.THIRD_PARTY_DIR, "openjdk"));
+          "1.1.5",
+          Paths.get(ToolHelper.THIRD_PARTY_DIR, "openjdk", "desugar_jdk_libs_releases"));
 
   public LegacyDesugaredLibraryBenchmark(BenchmarkConfig config, TestParameters parameters) {
     super(config, parameters);
@@ -47,8 +47,8 @@
             .setTarget(BenchmarkTarget.D8)
             .setFromRevision(12150)
             .setMethod(LegacyDesugaredLibraryBenchmark::run)
-            .addDependency(androidJar)
-            .addDependency(legacyConf)
+            .addDependency(ANDROID_JAR)
+            .addDependency(LEGACY_CONF)
             .measureRunTime()
             .build());
   }
@@ -62,15 +62,13 @@
             results ->
                 testForD8(environment.getTemp(), Backend.DEX)
                     .setMinApi(AndroidApiLevel.B)
-                    .addLibraryFiles(androidJar.getRoot(environment).resolve("android.jar"))
+                    .addLibraryFiles(ANDROID_JAR.getRoot(environment).resolve("android.jar"))
                     .apply(
                         b ->
                             b.getBuilder()
                                 .addDesugaredLibraryConfiguration(
                                     StringResource.fromFile(
-                                        legacyConf
-                                            .getRoot(environment)
-                                            .resolve("desugar_jdk_libs.json"))))
+                                        LEGACY_CONF.getRoot(environment).resolve("desugar.json"))))
                     .benchmarkCompile(results));
   }
 
diff --git a/src/test/java/com/android/tools/r8/cf/methodhandles/invokespecial/InvokeSpecialMethodHandleTest.java b/src/test/java/com/android/tools/r8/cf/methodhandles/invokespecial/InvokeSpecialMethodHandleTest.java
index 802ccce..fbbadb0 100644
--- a/src/test/java/com/android/tools/r8/cf/methodhandles/invokespecial/InvokeSpecialMethodHandleTest.java
+++ b/src/test/java/com/android/tools/r8/cf/methodhandles/invokespecial/InvokeSpecialMethodHandleTest.java
@@ -130,7 +130,12 @@
       } else if (parameters.isDexRuntime()
           && parameters.asDexRuntime().getVersion().isNewerThan(Version.V9_0_0)) {
         // VMs between 9 and 13 segfault.
-        result.assertFailureWithErrorThatMatches(containsString("HandleUnexpectedSignal"));
+        if (parameters.asDexRuntime().getVersion().isEqualTo(Version.V14_0_0)) {
+          result.assertFailureWithOutputThatMatches(
+              containsString("reverting to SIG_DFL handler for signal 11"));
+        } else {
+          result.assertFailureWithErrorThatMatches(containsString("HandleUnexpectedSignal"));
+        }
       } else {
         result.assertSuccessWithOutput(getExpected());
       }
diff --git a/src/test/java/com/android/tools/r8/cfmethodgeneration/InstructionTypeMapper.java b/src/test/java/com/android/tools/r8/cfmethodgeneration/InstructionTypeMapper.java
new file mode 100644
index 0000000..ab69d27
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/cfmethodgeneration/InstructionTypeMapper.java
@@ -0,0 +1,99 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.cfmethodgeneration;
+
+import com.android.tools.r8.cf.code.CfFieldInstruction;
+import com.android.tools.r8.cf.code.CfFrame;
+import com.android.tools.r8.cf.code.CfInstruction;
+import com.android.tools.r8.cf.code.CfInvoke;
+import com.android.tools.r8.cf.code.CfTypeInstruction;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexType;
+import java.util.Map;
+import java.util.function.Function;
+
+public class InstructionTypeMapper {
+
+  private final DexItemFactory factory;
+  private final Map<DexType, DexType> typeMap;
+  private final Function<String, String> methodNameMap;
+
+  public InstructionTypeMapper(
+      DexItemFactory factory,
+      Map<DexType, DexType> typeMap,
+      Function<String, String> methodNameMap) {
+    this.factory = factory;
+    this.typeMap = typeMap;
+    this.methodNameMap = methodNameMap;
+  }
+
+  public CfInstruction rewriteInstruction(CfInstruction instruction) {
+    if (instruction.isTypeInstruction()) {
+      CfInstruction rewritten = rewriteTypeInstruction(instruction.asTypeInstruction());
+      return rewritten == null ? instruction : rewritten;
+    }
+    if (instruction.isFieldInstruction()) {
+      return rewriteFieldInstruction(instruction.asFieldInstruction());
+    }
+    if (instruction.isInvoke()) {
+      return rewriteInvokeInstruction(instruction.asInvoke());
+    }
+    if (instruction.isFrame()) {
+      return rewriteFrameInstruction(instruction.asFrame());
+    }
+    return instruction;
+  }
+
+  private CfInstruction rewriteInvokeInstruction(CfInvoke instruction) {
+    CfInvoke invoke = instruction.asInvoke();
+    DexMethod method = invoke.getMethod();
+    String name = method.getName().toString();
+    DexType holderType = invoke.getMethod().getHolderType();
+    DexType rewrittenType = typeMap.getOrDefault(holderType, holderType);
+    String rewrittenName =
+        rewrittenType == factory.varHandleType ? methodNameMap.apply(name) : name;
+    if (rewrittenType != holderType) {
+      return new CfInvoke(
+          invoke.getOpcode(),
+          factory.createMethod(
+              rewrittenType,
+              rewriteProto(invoke.getMethod().getProto()),
+              factory.createString(rewrittenName)),
+          invoke.isInterface());
+    }
+    return instruction;
+  }
+
+  private DexProto rewriteProto(DexProto proto) {
+    return factory.createProto(
+        typeMap.getOrDefault(proto.returnType, proto.returnType),
+        proto.parameters.stream()
+            .map(type -> typeMap.getOrDefault(type, type))
+            .toArray(DexType[]::new));
+  }
+
+  private CfFieldInstruction rewriteFieldInstruction(CfFieldInstruction instruction) {
+    DexType holderType = instruction.getField().getHolderType();
+    DexType rewrittenHolderType = typeMap.getOrDefault(holderType, holderType);
+    DexType fieldType = instruction.getField().getType();
+    DexType rewrittenType = typeMap.getOrDefault(fieldType, fieldType);
+    if (rewrittenHolderType != holderType || rewrittenType != fieldType) {
+      return instruction.createWithField(
+          factory.createField(rewrittenHolderType, rewrittenType, instruction.getField().name));
+    }
+    return instruction;
+  }
+
+  private CfInstruction rewriteTypeInstruction(CfTypeInstruction instruction) {
+    DexType rewrittenType = typeMap.getOrDefault(instruction.getType(), instruction.getType());
+    return rewrittenType != instruction.getType() ? instruction.withType(rewrittenType) : null;
+  }
+
+  private CfInstruction rewriteFrameInstruction(CfFrame instruction) {
+    return instruction.asFrame().mapReferenceTypes(type -> typeMap.getOrDefault(type, type));
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/Base64Test.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/Base64Test.java
index 22eb7b5..4afc01b 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/Base64Test.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/Base64Test.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.desugar.desugaredlibrary;
 
 import static com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification.SPECIFICATIONS_WITH_CF2CF;
+import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.JDK11;
 import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.JDK11_PATH;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
@@ -33,7 +34,7 @@
   public static List<Object[]> data() {
     return buildParameters(
         getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build(),
-        ImmutableList.of(JDK11_PATH),
+        ImmutableList.of(JDK11, JDK11_PATH),
         SPECIFICATIONS_WITH_CF2CF);
   }
 
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DateTimeFormatterTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DateTimeFormatterTest.java
index 089553b..f1e10a1 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DateTimeFormatterTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DateTimeFormatterTest.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.ToolHelper.DexVm.Version;
 import com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification;
 import com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification;
+import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.StringUtils;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
@@ -69,11 +70,13 @@
     if (libraryDesugaringSpecification.hasTimeDesugaring(parameters)) {
       run.assertSuccessWithOutput(
           parameters.isDexRuntimeVersionNewerThanOrEqual(Version.V14_0_0)
+                  && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.U)
               ? expectedOutputDesugaredLibNNBSP
               : expectedOutputDesugaredLib);
     } else {
       run.assertSuccessWithOutput(
           parameters.isDexRuntimeVersionNewerThanOrEqual(Version.V14_0_0)
+                  && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.U)
               ? expectedOutputNNBSP
               : expectedOutput);
     }
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryDeterminismTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryDeterminismTest.java
index 8245517..d25f709 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryDeterminismTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryDeterminismTest.java
@@ -4,7 +4,7 @@
 package com.android.tools.r8.desugar.desugaredlibrary;
 
 import static com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification.D8_L8DEBUG;
-import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.getJdk8Jdk11;
+import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.getJdk8AndAll3Jdk11;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
@@ -36,7 +36,7 @@
   public static List<Object[]> data() {
     return buildParameters(
         getTestParameters().withDexRuntimes().withAllApiLevels().build(),
-        getJdk8Jdk11(),
+        getJdk8AndAll3Jdk11(),
         ImmutableList.of(D8_L8DEBUG));
   }
 
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryNavTypeTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryNavTypeTest.java
new file mode 100644
index 0000000..16b1f40
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryNavTypeTest.java
@@ -0,0 +1,239 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.desugar.desugaredlibrary;
+
+import static com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification.D8_L8DEBUG;
+import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.JDK11;
+import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.JDK11_PATH;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification;
+import com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableList;
+import java.time.LocalDate;
+import java.util.List;
+import java.util.function.Supplier;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class DesugaredLibraryNavTypeTest extends DesugaredLibraryTestBase {
+
+  private static final String EXPECTED_RESULT_FORMAT =
+      StringUtils.lines(
+          "java.lang.IllegalArgumentException",
+          "java.lang.RuntimeException java.lang.ClassNotFoundException java.time.XYZ",
+          "Nav class %s.time.LocalDate",
+          "Nav class %s.time.LocalDate[]",
+          "Nav class java.lang.Object",
+          "java.lang.IllegalArgumentException",
+          "java.lang.RuntimeException java.lang.ClassNotFoundException java.time.XYZ",
+          "Nav class %s.time.LocalDate",
+          "Nav class %s.time.LocalDate[]",
+          "Nav class java.lang.Object");
+  private static final String NAV_TYPE = "Landroidx/navigation/NavType;";
+  private static final String NAV_TYPE_COMPANION = "Landroidx/navigation/NavType$Companion;";
+
+  private final TestParameters parameters;
+  private final CompilationSpecification compilationSpecification;
+  private final LibraryDesugaringSpecification libraryDesugaringSpecification;
+
+  @Parameters(name = "{0}, spec: {1}, {2}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withDexRuntimes().withAllApiLevels().build(),
+        ImmutableList.of(JDK11, JDK11_PATH),
+        ImmutableList.of(D8_L8DEBUG));
+  }
+
+  public DesugaredLibraryNavTypeTest(
+      TestParameters parameters,
+      LibraryDesugaringSpecification libraryDesugaringSpecification,
+      CompilationSpecification compilationSpecification) {
+    this.parameters = parameters;
+    this.compilationSpecification = compilationSpecification;
+    this.libraryDesugaringSpecification = libraryDesugaringSpecification;
+  }
+
+  @Test
+  public void testNavType() throws Throwable {
+    testForDesugaredLibrary(parameters, libraryDesugaringSpecification, compilationSpecification)
+        .addProgramClassFileData(getMain(), getNavType(), getNavTypeCompanion())
+        .addKeepMainRule(Main.class)
+        .addKeepRules(
+            "-keep class"
+                + " com.android.tools.r8.desugar.desugaredlibrary.DesugaredLibraryNavTypeTest.Main"
+                + " { public static LocalDate MIN; }")
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutput(
+            String.format(
+                EXPECTED_RESULT_FORMAT, getPrefix(), getPrefix(), getPrefix(), getPrefix()));
+  }
+
+  private String getPrefix() {
+    return parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.O) ? "java" : "j$";
+  }
+
+  public static byte[] getNavTypeCompanion() throws Exception {
+    return transformer(NavType.Companion.class)
+        .setClassDescriptor(NAV_TYPE_COMPANION)
+        .replaceClassDescriptorInMembers(descriptor(NavType.class), NAV_TYPE)
+        .replaceClassDescriptorInMethodInstructions(descriptor(NavType.class), NAV_TYPE)
+        .replaceClassDescriptorInMethodInstructions(
+            descriptor(NavType.Companion.class), NAV_TYPE_COMPANION)
+        .transform();
+  }
+
+  public static byte[] getNavType() throws Exception {
+    return transformer(NavType.class)
+        .setClassDescriptor(NAV_TYPE)
+        .replaceClassDescriptorInMembers(descriptor(NavType.Companion.class), NAV_TYPE_COMPANION)
+        .replaceClassDescriptorInMethodInstructions(descriptor(NavType.class), NAV_TYPE)
+        .replaceClassDescriptorInMethodInstructions(
+            descriptor(NavType.Companion.class), NAV_TYPE_COMPANION)
+        .transform();
+  }
+
+  public static byte[] getMain() throws Exception {
+    return transformer(Main.class)
+        .replaceClassDescriptorInMethodInstructions(descriptor(NavType.class), NAV_TYPE)
+        .replaceClassDescriptorInMethodInstructions(
+            descriptor(NavType.Companion.class), NAV_TYPE_COMPANION)
+        .transform();
+  }
+
+  public static class Main {
+
+    // The field should be kept to force keep it on desugared library.
+    public static LocalDate MIN = LocalDate.MIN;
+
+    public static void main(String[] args) {
+      testCallCompanion();
+      testCallStatic();
+    }
+
+    private static void testCallStatic() {
+      // Test IAE are correctly rethrown.
+      try {
+        System.out.println(NavType.fromArgType("IAETest", null));
+      } catch (Throwable t) {
+        System.out.println(t.getClass().getName());
+      }
+      // Test missing class is still missing with the correct error.
+      try {
+        System.out.println(
+            NavType.fromArgType("java.time.XYZ", "com.android.tools.r8.desugar.desugaredlibrary"));
+      } catch (Throwable t) {
+        System.out.println(
+            t.getClass().getName()
+                + " "
+                + t.getCause().getClass().getName()
+                + " "
+                + t.getCause().getMessage());
+      }
+      // Test class is present with the retargeting and desugared library.
+      System.out.println(
+          NavType.fromArgType(
+              "java.time.LocalDate", "com.android.tools.r8.desugar.desugaredlibrary"));
+      // Test array class is present with the retargeting and desugared library.
+      System.out.println(
+          NavType.fromArgType(
+              "java.time.LocalDate[]", "com.android.tools.r8.desugar.desugaredlibrary"));
+      // Test always present class.
+      System.out.println(
+          NavType.fromArgType("java.lang.Object", "com.android.tools.r8.desugar.desugaredlibrary"));
+    }
+
+    private static void testCallCompanion() {
+      // Test IAE are correctly rethrown.
+      try {
+        System.out.println(NavType.Companion.fromArgType("IAETest", null));
+      } catch (Throwable t) {
+        System.out.println(t.getClass().getName());
+      }
+      // Test missing class is still missing with the correct error.
+      try {
+        System.out.println(
+            NavType.Companion.fromArgType(
+                "java.time.XYZ", "com.android.tools.r8.desugar.desugaredlibrary"));
+      } catch (Throwable t) {
+        System.out.println(
+            t.getClass().getName()
+                + " "
+                + t.getCause().getClass().getName()
+                + " "
+                + t.getCause().getMessage());
+      }
+      // Test class is present with the retargeting and desugared library.
+      System.out.println(
+          NavType.Companion.fromArgType(
+              "java.time.LocalDate", "com.android.tools.r8.desugar.desugaredlibrary"));
+      // Test array class is present with the retargeting and desugared library.
+      System.out.println(
+          NavType.Companion.fromArgType(
+              "java.time.LocalDate[]", "com.android.tools.r8.desugar.desugaredlibrary"));
+      // Test always present class.
+      System.out.println(
+          NavType.Companion.fromArgType(
+              "java.lang.Object", "com.android.tools.r8.desugar.desugaredlibrary"));
+    }
+
+    public String run(Supplier<String> supplier) {
+      try {
+        return supplier.get();
+      } catch (Throwable t) {
+        return t.getClass().toString();
+      }
+    }
+  }
+
+  // Will be rewritten to androidx/navigation/NavType.
+  public static class NavType {
+
+    public static final Companion Companion = new Companion();
+
+    private final Class<?> clazz;
+    private final boolean isArray;
+
+    public NavType(Class<?> clazz, boolean isArray) {
+      this.clazz = clazz;
+      this.isArray = isArray;
+    }
+
+    @Override
+    public String toString() {
+      return "Nav " + clazz.toString() + (isArray ? "[]" : "");
+    }
+
+    public static NavType fromArgType(String type, String pkgName) {
+      return Companion.fromArgType(type, pkgName);
+    }
+
+    public static class Companion {
+
+      public NavType fromArgType(String type, String pkgName) {
+        try {
+          String className = (type.startsWith(".") && pkgName != null) ? pkgName + type : type;
+          if (type.equals("IAETest")) {
+            throw new IllegalArgumentException(className + " is not Serializable or Parcelable.");
+          } else {
+            if (type.endsWith("[]")) {
+              className = className.substring(0, className.length() - 2);
+              return new NavType(Class.forName(className), true);
+            } else {
+              return new NavType(Class.forName(className), false);
+            }
+          }
+        } catch (ClassNotFoundException e) {
+          throw new RuntimeException(e);
+        }
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/generation/DesugaredLibraryBridge.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/generation/DesugaredLibraryBridge.java
new file mode 100644
index 0000000..2d7ff31
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/generation/DesugaredLibraryBridge.java
@@ -0,0 +1,35 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.desugar.desugaredlibrary.generation;
+
+public class DesugaredLibraryBridge {
+
+  public static NavType<?> fromArgType(
+      NavType.Companion companion, String type, String packageName) {
+    if (type == null || !type.startsWith("java")) {
+      return companion.fromArgType(type, packageName);
+    }
+    // With desugared library j$ types take precedence over java types.
+    try {
+      return companion.fromArgType("j$" + type.substring("java".length()), packageName);
+    } catch (RuntimeException e) {
+      if (e.getCause() instanceof ClassNotFoundException) {
+        return companion.fromArgType(type, packageName);
+      }
+      throw e;
+    }
+  }
+
+  public static class NavType<T> {
+
+    public static final Companion Companion = new Companion();
+
+    public static class Companion {
+      public NavType<?> fromArgType(String type, String packageName) {
+        return null;
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/generation/GenerateDesugaredLibraryBridge.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/generation/GenerateDesugaredLibraryBridge.java
new file mode 100644
index 0000000..bf45240
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/generation/GenerateDesugaredLibraryBridge.java
@@ -0,0 +1,97 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.desugar.desugaredlibrary.generation;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.cfmethodgeneration.InstructionTypeMapper;
+import com.android.tools.r8.cfmethodgeneration.MethodGenerationBase;
+import com.android.tools.r8.desugar.desugaredlibrary.generation.DesugaredLibraryBridge.NavType;
+import com.android.tools.r8.graph.CfCode;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.FileUtils;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class GenerateDesugaredLibraryBridge extends MethodGenerationBase {
+
+  private final DexType GENERATED_TYPE =
+      factory.createType(
+          "Lcom/android/tools/r8/ir/desugar/desugaredlibrary/retargeter/DesugaredLibraryCfMethods;");
+  private final List<Class<?>> METHOD_TEMPLATE_CLASSES =
+      ImmutableList.of(DesugaredLibraryBridge.class);
+
+  protected final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withCfRuntime(CfVm.JDK9).build();
+  }
+
+  public GenerateDesugaredLibraryBridge(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Override
+  protected DexType getGeneratedType() {
+    return GENERATED_TYPE;
+  }
+
+  @Override
+  protected List<Class<?>> getMethodTemplateClasses() {
+    return METHOD_TEMPLATE_CLASSES;
+  }
+
+  @Override
+  protected int getYear() {
+    return 2023;
+  }
+
+  @Test
+  public void testDesugaredLibraryBridge() throws Exception {
+    ArrayList<Class<?>> sorted = new ArrayList<>(getMethodTemplateClasses());
+    sorted.sort(Comparator.comparing(Class::getTypeName));
+    assertEquals("Classes should be listed in sorted order", sorted, getMethodTemplateClasses());
+    assertEquals(
+        FileUtils.readTextFile(getGeneratedFile(), StandardCharsets.UTF_8), generateMethods());
+  }
+
+  @Override
+  protected CfCode getCode(String holderName, String methodName, CfCode code) {
+    InstructionTypeMapper instructionTypeMapper =
+        new InstructionTypeMapper(
+            factory,
+            ImmutableMap.of(
+                factory.createType(DescriptorUtils.javaClassToDescriptor(NavType.class)),
+                factory.createType("Landroidx/navigation/NavType;"),
+                factory.createType(DescriptorUtils.javaClassToDescriptor(NavType.Companion.class)),
+                factory.createType("Landroidx/navigation/NavType$Companion;")),
+            Function.identity());
+    code.setInstructions(
+        code.getInstructions().stream()
+            .map(instructionTypeMapper::rewriteInstruction)
+            .collect(Collectors.toList()));
+    return code;
+  }
+
+  public static void main(String[] args) throws Exception {
+    new GenerateDesugaredLibraryBridge(null).generateMethodsAndWriteThemToFile();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/test/LibraryDesugaringSpecification.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/test/LibraryDesugaringSpecification.java
index b82ba3e..de43daf 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/test/LibraryDesugaringSpecification.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/test/LibraryDesugaringSpecification.java
@@ -29,6 +29,7 @@
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.ExecutorService;
+import java.util.function.Supplier;
 import org.junit.rules.TemporaryFolder;
 
 public class LibraryDesugaringSpecification {
@@ -91,22 +92,27 @@
     LATEST
   }
 
-  private static final Path tempLibraryJDK11Undesugar = createUndesugaredJdk11LibJarForTesting();
+  private static Path tempLibraryJdk11UndesugarCache;
 
-  private static Path createUndesugaredJdk11LibJarForTesting() {
+  private static synchronized Path ensureUndesugaredJdk11LibJarForTesting() {
+    if (tempLibraryJdk11UndesugarCache != null) {
+      return tempLibraryJdk11UndesugarCache;
+    }
     try {
       TemporaryFolder staticTemp = ToolHelper.getTemporaryFolderForTest();
       staticTemp.create();
       Path jdklib_desugaring = staticTemp.newFolder("jdklib_desugaring").toPath();
-      return DesugaredLibraryJDK11Undesugarer.undesugaredJarJDK11(
-          jdklib_desugaring, DESUGARED_JDK_11_LIB_JAR);
+      tempLibraryJdk11UndesugarCache =
+          DesugaredLibraryJDK11Undesugarer.undesugaredJarJDK11(
+              jdklib_desugaring, DESUGARED_JDK_11_LIB_JAR);
+      return tempLibraryJdk11UndesugarCache;
     } catch (IOException e) {
       throw new RuntimeException(e);
     }
   }
 
   public static Path getTempLibraryJDK11Undesugar() {
-    return tempLibraryJDK11Undesugar;
+    return ensureUndesugaredJdk11LibJarForTesting();
   }
 
   // Main head specifications.
@@ -121,7 +127,7 @@
   public static LibraryDesugaringSpecification JDK11 =
       new LibraryDesugaringSpecification(
           "JDK11",
-          tempLibraryJDK11Undesugar,
+          LibraryDesugaringSpecification::ensureUndesugaredJdk11LibJarForTesting,
           "jdk11/desugar_jdk_libs.json",
           AndroidApiLevel.R,
           JDK11_DESCRIPTOR,
@@ -129,7 +135,7 @@
   public static LibraryDesugaringSpecification JDK11_MINIMAL =
       new LibraryDesugaringSpecification(
           "JDK11_MINIMAL",
-          tempLibraryJDK11Undesugar,
+          LibraryDesugaringSpecification::ensureUndesugaredJdk11LibJarForTesting,
           "jdk11/desugar_jdk_libs_minimal.json",
           AndroidApiLevel.R,
           EMPTY_DESCRIPTOR_24,
@@ -137,7 +143,7 @@
   public static LibraryDesugaringSpecification JDK11_PATH =
       new LibraryDesugaringSpecification(
           "JDK11_PATH",
-          tempLibraryJDK11Undesugar,
+          LibraryDesugaringSpecification::ensureUndesugaredJdk11LibJarForTesting,
           "jdk11/desugar_jdk_libs_nio.json",
           AndroidApiLevel.R,
           JDK11_PATH_DESCRIPTOR,
@@ -165,7 +171,8 @@
       new LibraryDesugaringSpecification("1.1.5", AndroidApiLevel.P);
 
   private final String name;
-  private final Set<Path> desugarJdkLibs;
+  private final Set<Supplier<Path>> desugarJdkLibsProvider;
+  private Set<Path> desugarJdkLibs;
   private final Path specification;
   private final Set<Path> libraryFiles;
   private final Descriptor descriptor;
@@ -195,7 +202,36 @@
       Set<Path> libraryFiles,
       Descriptor descriptor,
       String extraKeepRules) {
+    this(name, null, desugarJdkLibs, specification, libraryFiles, descriptor, extraKeepRules);
+  }
+
+  private LibraryDesugaringSpecification(
+      String name,
+      Supplier<Path> desugarJdkLibsSupplier,
+      String specificationPath,
+      AndroidApiLevel androidJarLevel,
+      Descriptor descriptor,
+      CustomConversionVersion legacy) {
+    this(
+        name,
+        ImmutableSet.of(desugarJdkLibsSupplier, () -> ToolHelper.getDesugarLibConversions(legacy)),
+        null,
+        Paths.get(ToolHelper.LIBRARY_DESUGAR_SOURCE_DIR + specificationPath),
+        ImmutableSet.of(ToolHelper.getAndroidJar(androidJarLevel)),
+        descriptor,
+        "");
+  }
+
+  private LibraryDesugaringSpecification(
+      String name,
+      Set<Supplier<Path>> desugarJdkLibsSuppliers,
+      Set<Path> desugarJdkLibs,
+      Path specification,
+      Set<Path> libraryFiles,
+      Descriptor descriptor,
+      String extraKeepRules) {
     this.name = name;
+    this.desugarJdkLibsProvider = desugarJdkLibsSuppliers;
     this.desugarJdkLibs = desugarJdkLibs;
     this.specification = specification;
     this.libraryFiles = libraryFiles;
@@ -220,7 +256,14 @@
     return name;
   }
 
-  public Set<Path> getDesugarJdkLibs() {
+  public synchronized Set<Path> getDesugarJdkLibs() {
+    if (desugarJdkLibs == null) {
+      ImmutableSet.Builder<Path> builder = ImmutableSet.builder();
+      for (Supplier<Path> pathSupplier : desugarJdkLibsProvider) {
+        builder.add(pathSupplier.get());
+      }
+      desugarJdkLibs = builder.build();
+    }
     return desugarJdkLibs;
   }
 
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordComponentAnnotationsTest.java b/src/test/java/com/android/tools/r8/desugar/records/RecordComponentAnnotationsTest.java
index dc3ef4e..14485c9 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/RecordComponentAnnotationsTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordComponentAnnotationsTest.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestRuntime.CfVm;
 import com.android.tools.r8.TestShrinkerBuilder;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.Pair;
@@ -35,7 +36,7 @@
   private static final String RECORD_NAME = "RecordWithAnnotations";
   private static final byte[][] PROGRAM_DATA = RecordTestUtils.getProgramData(RECORD_NAME);
   private static final String MAIN_TYPE = RecordTestUtils.getMainType(RECORD_NAME);
-  private static final String EXPECTED_RESULT =
+  private static final String JVM_EXPECTED_RESULT =
       StringUtils.lines(
           "Jane Doe",
           "42",
@@ -57,12 +58,39 @@
           "@records.RecordWithAnnotations$AnnotationRecordComponentOnly(\"z\")",
           "2",
           "2",
-          "@records.RecordWithAnnotations$Annotation(\"a\")",
-          "@records.RecordWithAnnotations$AnnotationFieldOnly(\"b\")",
-          "2",
           "@records.RecordWithAnnotations$Annotation(\"x\")",
-          "@records.RecordWithAnnotations$AnnotationFieldOnly(\"y\")");
-  private static final String EXPECTED_RESULT_R8 =
+          "@records.RecordWithAnnotations$AnnotationFieldOnly(\"y\")",
+          "2",
+          "@records.RecordWithAnnotations$Annotation(\"a\")",
+          "@records.RecordWithAnnotations$AnnotationFieldOnly(\"b\")");
+  private static final String ART_EXPECTED_RESULT =
+      StringUtils.lines(
+          "Jane Doe",
+          "42",
+          "Jane Doe",
+          "42",
+          "true",
+          "2",
+          "name",
+          "java.lang.String",
+          "true",
+          "2",
+          "@records.RecordWithAnnotations$Annotation(value=a)",
+          "@records.RecordWithAnnotations$AnnotationRecordComponentOnly(value=c)",
+          "age",
+          "int",
+          "true",
+          "2",
+          "@records.RecordWithAnnotations$Annotation(value=x)",
+          "@records.RecordWithAnnotations$AnnotationRecordComponentOnly(value=z)",
+          "2",
+          "2",
+          "@records.RecordWithAnnotations$Annotation(value=x)",
+          "@records.RecordWithAnnotations$AnnotationFieldOnly(value=y)",
+          "2",
+          "@records.RecordWithAnnotations$Annotation(value=a)",
+          "@records.RecordWithAnnotations$AnnotationFieldOnly(value=b)");
+  private static final String JVM_EXPECTED_RESULT_R8 =
       StringUtils.lines(
           "Jane Doe",
           "42",
@@ -89,7 +117,34 @@
           "2",
           "@records.RecordWithAnnotations$Annotation(\"x\")",
           "@records.RecordWithAnnotations$AnnotationFieldOnly(\"y\")");
-  private static final String EXPECTED_RESULT_R8_NO_KEEP_ANNOTATIONS =
+  private static final String ART_EXPECTED_RESULT_R8 =
+      StringUtils.lines(
+          "Jane Doe",
+          "42",
+          "Jane Doe",
+          "42",
+          "true",
+          "2",
+          "a",
+          "java.lang.String",
+          "true",
+          "2",
+          "@records.RecordWithAnnotations$Annotation(value=a)",
+          "@records.RecordWithAnnotations$AnnotationRecordComponentOnly(value=c)",
+          "b",
+          "int",
+          "true",
+          "2",
+          "@records.RecordWithAnnotations$Annotation(value=x)",
+          "@records.RecordWithAnnotations$AnnotationRecordComponentOnly(value=z)",
+          "2",
+          "2",
+          "@records.RecordWithAnnotations$Annotation(value=a)",
+          "@records.RecordWithAnnotations$AnnotationFieldOnly(value=b)",
+          "2",
+          "@records.RecordWithAnnotations$Annotation(value=x)",
+          "@records.RecordWithAnnotations$AnnotationFieldOnly(value=y)");
+  private static final String JVM_EXPECTED_RESULT_R8_NO_KEEP_ANNOTATIONS =
       StringUtils.lines(
           "Jane Doe",
           "42",
@@ -112,10 +167,33 @@
           "2",
           "0",
           "0");
-  private static final String EXPECTED_RESULT_DESUGARED =
-      StringUtils.lines("Jane Doe", "42", "Jane Doe", "42", "Class.isRecord not present");
-  private static final String EXPECTED_RESULT_DESUGARED_JVM17 =
+  private static final String ART_EXPECTED_RESULT_R8_NO_KEEP_ANNOTATIONS =
+      StringUtils.lines(
+          "Jane Doe",
+          "42",
+          "Jane Doe",
+          "42",
+          "true",
+          "2",
+          "a",
+          "java.lang.String",
+          "true",
+          "2",
+          "@records.RecordWithAnnotations$Annotation(value=a)",
+          "@records.RecordWithAnnotations$AnnotationRecordComponentOnly(value=c)",
+          "b",
+          "int",
+          "true",
+          "2",
+          "@records.RecordWithAnnotations$Annotation(value=x)",
+          "@records.RecordWithAnnotations$AnnotationRecordComponentOnly(value=z)",
+          "2",
+          "0",
+          "0");
+  private static final String EXPECTED_RESULT_DESUGARED_RECORD_SUPPORT =
       StringUtils.lines("Jane Doe", "42", "Jane Doe", "42", "false");
+  private static final String EXPECTED_RESULT_DESUGARED_NO_RECORD_SUPPORT =
+      StringUtils.lines("Jane Doe", "42", "Jane Doe", "42", "Class.isRecord not present");
 
   @Parameter(0)
   public TestParameters parameters;
@@ -141,7 +219,7 @@
     testForJvm(parameters)
         .addProgramClassFileData(PROGRAM_DATA)
         .run(parameters.getRuntime(), MAIN_TYPE)
-        .assertSuccessWithOutput(EXPECTED_RESULT);
+        .assertSuccessWithOutput(JVM_EXPECTED_RESULT);
   }
 
   @Test
@@ -151,6 +229,8 @@
     // Android U will support records.
     boolean compilingForNativeRecordSupport =
         parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.U);
+    boolean runtimeWithNativeRecordSupport =
+        parameters.getDexRuntimeVersion().isNewerThanOrEqual(Version.V14_0_0);
     testForDesugaring(
             parameters,
             options -> {
@@ -164,16 +244,13 @@
         .addProgramClassFileData(PROGRAM_DATA)
         .run(parameters.getRuntime(), MAIN_TYPE)
         .applyIf(
-            parameters.isDexRuntime() && compilingForNativeRecordSupport,
-            // Current Art 14 build does not support the java.lang.Record class.
-            r -> r.assertFailureWithErrorThatThrows(NoClassDefFoundError.class),
             compilingForNativeRecordSupport,
-            r -> r.assertSuccessWithOutput(EXPECTED_RESULT),
+            r -> r.assertSuccessWithOutput(ART_EXPECTED_RESULT),
             r ->
                 r.assertSuccessWithOutput(
-                        !parameters.isCfRuntime()
-                            ? EXPECTED_RESULT_DESUGARED
-                            : EXPECTED_RESULT_DESUGARED_JVM17)
+                        !runtimeWithNativeRecordSupport
+                            ? EXPECTED_RESULT_DESUGARED_NO_RECORD_SUPPORT
+                            : EXPECTED_RESULT_DESUGARED_RECORD_SUPPORT)
                     .inspect(
                         inspector -> {
                           ClassSubject person =
@@ -247,6 +324,9 @@
     boolean compilingForNativeRecordSupport =
         parameters.isCfRuntime()
             || parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.U);
+    boolean runtimeWithNativeRecordSupport =
+        parameters.isCfRuntime()
+            || parameters.getDexRuntimeVersion().isNewerThanOrEqual(Version.V14_0_0);
     testForR8(parameters.getBackend())
         .addProgramClassFileData(PROGRAM_DATA)
         // TODO(b/231930852): Change to android.jar for Androud U when that contains
@@ -317,16 +397,24 @@
             })
         .run(parameters.getRuntime(), MAIN_TYPE)
         .applyIf(
-            parameters.isCfRuntime(),
             // TODO(b/274888318): EXPECTED_RESULT_R8_NO_KEEP_ANNOTATIONS still has component
             //  annotations.
+            parameters.isCfRuntime(),
             r ->
                 r.assertSuccessWithOutput(
-                    keepAnnotations ? EXPECTED_RESULT_R8 : EXPECTED_RESULT_R8_NO_KEEP_ANNOTATIONS),
-            // r -> r.assertSuccessWithOutput(EXPECTED_RESULT_R8),
+                    keepAnnotations
+                        ? JVM_EXPECTED_RESULT_R8
+                        : JVM_EXPECTED_RESULT_R8_NO_KEEP_ANNOTATIONS),
             compilingForNativeRecordSupport,
-            // Current Art 14 build does not support the java.lang.Record class.
-            r -> r.assertFailureWithErrorThatThrows(NoClassDefFoundError.class),
-            r -> r.assertSuccessWithOutput(EXPECTED_RESULT_DESUGARED));
+            r ->
+                r.assertSuccessWithOutput(
+                    keepAnnotations
+                        ? ART_EXPECTED_RESULT_R8
+                        : ART_EXPECTED_RESULT_R8_NO_KEEP_ANNOTATIONS),
+            r ->
+                r.assertSuccessWithOutput(
+                    runtimeWithNativeRecordSupport
+                        ? EXPECTED_RESULT_DESUGARED_RECORD_SUPPORT
+                        : EXPECTED_RESULT_DESUGARED_NO_RECORD_SUPPORT));
   }
 }
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordComponentSignatureTest.java b/src/test/java/com/android/tools/r8/desugar/records/RecordComponentSignatureTest.java
index 5ce845f..db46699 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/RecordComponentSignatureTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordComponentSignatureTest.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.desugar.records;
 
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
@@ -14,6 +15,7 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestRuntime.CfVm;
 import com.android.tools.r8.TestShrinkerBuilder;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.StringUtils;
@@ -56,6 +58,8 @@
           "Jane Doe", "42", "Jane Doe", "42", "true", "1", "a", "java.lang.Object", "null", "0");
   private static final String EXPECTED_RESULT_DESUGARED =
       StringUtils.lines("Jane Doe", "42", "Jane Doe", "42", "Class.isRecord not present");
+  private static final String EXPECTED_RESULT_DESUGARED_ART_14 =
+      StringUtils.lines("Jane Doe", "42", "Jane Doe", "42", "false");
 
   @Parameter(0)
   public TestParameters parameters;
@@ -91,6 +95,9 @@
     // Android U will support records.
     boolean compilingForNativeRecordSupport =
         parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.U);
+    boolean runningWithNativeRecordSupport =
+        parameters.getRuntime().isDex()
+            && parameters.getRuntime().asDex().getVersion().isNewerThanOrEqual(Version.V14_0_0);
     testForDesugaring(
             parameters,
             options -> {
@@ -106,9 +113,12 @@
         .applyIf(
             compilingForNativeRecordSupport,
             // Current Art 14 build does not support the java.lang.Record class.
-            r -> r.assertFailureWithErrorThatThrows(NoClassDefFoundError.class),
+            r -> r.assertSuccessWithOutput(EXPECTED_RESULT),
             r ->
-                r.assertSuccessWithOutput(EXPECTED_RESULT_DESUGARED)
+                r.assertSuccessWithOutput(
+                        runningWithNativeRecordSupport
+                            ? EXPECTED_RESULT_DESUGARED_ART_14
+                            : EXPECTED_RESULT_DESUGARED)
                     .inspect(
                         inspector -> {
                           ClassSubject person =
@@ -154,6 +164,9 @@
     boolean compilingForNativeRecordSupport =
         parameters.isCfRuntime()
             || parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.U);
+    boolean runningWithNativeRecordSupport =
+        parameters.getRuntime().isDex()
+            && parameters.getRuntime().asDex().getVersion().isNewerThanOrEqual(Version.V14_0_0);
     testForR8(parameters.getBackend())
         .addProgramClassFileData(PROGRAM_DATA)
         // TODO(b/231930852): Change to android.jar for Androud U when that contains
@@ -173,8 +186,8 @@
             inspector -> {
               ClassSubject person = inspector.clazz("records.RecordWithSignature$Person");
               FieldSubject age = person.uniqueFieldWithOriginalName("age");
-              assertThat(age, isPresentAndRenamed());
               if (compilingForNativeRecordSupport) {
+                assertThat(age, isPresentAndRenamed());
                 assertEquals(1, person.getFinalRecordComponents().size());
                 assertEquals(
                     age.getFinalName(), person.getFinalRecordComponents().get(0).getName());
@@ -187,19 +200,21 @@
                 }
                 assertEquals(0, person.getFinalRecordComponents().get(0).getAnnotations().size());
               } else {
+                assertThat(age, isAbsent());
                 assertEquals(0, person.getFinalRecordComponents().size());
               }
             })
         .run(parameters.getRuntime(), MAIN_TYPE)
         // No Art VM actually supports the java.lang.Record class.
         .applyIf(
-            parameters.isCfRuntime(),
+            compilingForNativeRecordSupport,
             r ->
                 r.assertSuccessWithOutput(
                     keepSignatures ? EXPECTED_RESULT_R8 : EXPECTED_RESULT_R8_NO_KEEP_SIGNATURE),
-            compilingForNativeRecordSupport,
-            // Current Art 14 build does not support the java.lang.Record class.
-            r -> r.assertFailureWithErrorThatThrows(NoClassDefFoundError.class),
-            r -> r.assertSuccessWithOutput(EXPECTED_RESULT_DESUGARED));
+            r ->
+                r.assertSuccessWithOutput(
+                    runningWithNativeRecordSupport
+                        ? EXPECTED_RESULT_DESUGARED_ART_14
+                        : EXPECTED_RESULT_DESUGARED));
   }
 }
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordInvokeCustomSplitDesugaringTest.java b/src/test/java/com/android/tools/r8/desugar/records/RecordInvokeCustomSplitDesugaringTest.java
index 3e1c8ce..00a590e 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/RecordInvokeCustomSplitDesugaringTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordInvokeCustomSplitDesugaringTest.java
@@ -4,9 +4,13 @@
 
 package com.android.tools.r8.desugar.records;
 
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
+import static org.hamcrest.CoreMatchers.containsString;
+
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.StringUtils;
 import java.nio.file.Path;
 import org.junit.Test;
@@ -77,7 +81,20 @@
         .addProgramFiles(desugared)
         .setMinApi(parameters)
         .addKeepMainRule(MAIN_TYPE)
-        .compile()
+        .applyIf(
+            parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.U),
+            b -> b.allowDiagnosticWarningMessages())
+        .compileWithExpectedDiagnostics(
+            // Type com.android.tools.r8.RecordTag from desugared code will be converted to
+            // java.lang.Record during reading causing duplicate java.lang.Record class.
+            parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.U)
+                ? diagnostics ->
+                    diagnostics.assertWarningsMatch(
+                        diagnosticMessage(
+                            containsString(
+                                "The following library types, prefixed by java., are present both"
+                                    + " as library and non library classes: java.lang.Record.")))
+                : diagnostics -> diagnostics.assertNoMessages())
         .run(parameters.getRuntime(), MAIN_TYPE)
         .assertSuccessWithOutput(EXPECTED_RESULT_R8);
   }
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/kotlin/SimpleKotlinEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/kotlin/SimpleKotlinEnumUnboxingTest.java
index fcbbf70..d97a313 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/kotlin/SimpleKotlinEnumUnboxingTest.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/kotlin/SimpleKotlinEnumUnboxingTest.java
@@ -72,9 +72,7 @@
         .addKeepRules(enumKeepRules.getKeepRules())
         .addKeepRuntimeVisibleAnnotations()
         .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
-        // TODO(b/268005228): We should be able to unbox.
-        .addEnumUnboxingInspector(
-            inspector -> inspector.assertUnboxedIf(!kotlinParameters.isKotlinDev(), PKG + ".Color"))
+        .addEnumUnboxingInspector(inspector -> inspector.assertUnboxed(PKG + ".Color"))
         .allowDiagnosticMessages()
         .setMinApi(parameters)
         .compile()
diff --git a/src/test/java/com/android/tools/r8/globalsynthetics/GlobalSyntheticsEnsureClassesOutputTest.java b/src/test/java/com/android/tools/r8/globalsynthetics/GlobalSyntheticsEnsureClassesOutputTest.java
index bd06608..bda012e 100644
--- a/src/test/java/com/android/tools/r8/globalsynthetics/GlobalSyntheticsEnsureClassesOutputTest.java
+++ b/src/test/java/com/android/tools/r8/globalsynthetics/GlobalSyntheticsEnsureClassesOutputTest.java
@@ -50,7 +50,7 @@
             .setProgramConsumer(new DexIndexedConsumer.ArchiveConsumer(output))
             .build());
     CodeInspector inspector = new CodeInspector(output);
-    assertEquals(1026, inspector.allClasses().size());
+    assertEquals(1044, inspector.allClasses().size());
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/ir/desugar/varhandle/GenerateVarHandleMethods.java b/src/test/java/com/android/tools/r8/ir/desugar/varhandle/GenerateVarHandleMethods.java
index 1823867..e5434a0 100644
--- a/src/test/java/com/android/tools/r8/ir/desugar/varhandle/GenerateVarHandleMethods.java
+++ b/src/test/java/com/android/tools/r8/ir/desugar/varhandle/GenerateVarHandleMethods.java
@@ -9,16 +9,11 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.TestRuntime.CfVm;
-import com.android.tools.r8.cf.code.CfFieldInstruction;
-import com.android.tools.r8.cf.code.CfFrame;
-import com.android.tools.r8.cf.code.CfInstruction;
-import com.android.tools.r8.cf.code.CfInvoke;
-import com.android.tools.r8.cf.code.CfTypeInstruction;
+import com.android.tools.r8.cfmethodgeneration.InstructionTypeMapper;
 import com.android.tools.r8.cfmethodgeneration.MethodGenerationBase;
 import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.utils.DescriptorUtils;
@@ -30,9 +25,7 @@
 import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
-import java.util.function.Function;
 import java.util.stream.Collectors;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -95,74 +88,6 @@
     return field;
   }
 
-  // TODO(b/261024278): Share this code.
-  private class InstructionTypeMapper {
-    private final Map<DexType, DexType> typeMap;
-    private final Function<String, String> methodNameMap;
-
-    InstructionTypeMapper(Map<DexType, DexType> typeMap, Function<String, String> methodNameMap) {
-      this.typeMap = typeMap;
-      this.methodNameMap = methodNameMap;
-    }
-
-    private CfInstruction rewriteInstruction(CfInstruction instruction) {
-      if (instruction.isTypeInstruction()) {
-        CfInstruction rewritten = rewriteTypeInstruction(instruction.asTypeInstruction());
-        return rewritten == null ? instruction : rewritten;
-      }
-      if (instruction.isFieldInstruction()) {
-        return rewriteFieldInstruction(instruction.asFieldInstruction());
-      }
-      if (instruction.isInvoke()) {
-        return rewriteInvokeInstruction(instruction.asInvoke());
-      }
-      if (instruction.isFrame()) {
-        return rewriteFrameInstruction(instruction.asFrame());
-      }
-      return instruction;
-    }
-
-    private CfInstruction rewriteInvokeInstruction(CfInvoke instruction) {
-      CfInvoke invoke = instruction.asInvoke();
-      DexMethod method = invoke.getMethod();
-      String name = method.getName().toString();
-      DexType holderType = invoke.getMethod().getHolderType();
-      DexType rewrittenType = typeMap.getOrDefault(holderType, holderType);
-      String rewrittenName =
-          rewrittenType == factory.varHandleType ? methodNameMap.apply(name) : name;
-      if (rewrittenType != holderType) {
-        // TODO(b/261024278): If sharing this code also rewrite signature.
-        return new CfInvoke(
-            invoke.getOpcode(),
-            factory.createMethod(
-                rewrittenType, invoke.getMethod().getProto(), factory.createString(rewrittenName)),
-            invoke.isInterface());
-      }
-      return instruction;
-    }
-
-    private CfFieldInstruction rewriteFieldInstruction(CfFieldInstruction instruction) {
-      DexType holderType = instruction.getField().getHolderType();
-      DexType rewrittenHolderType = typeMap.getOrDefault(holderType, holderType);
-      DexType fieldType = instruction.getField().getType();
-      DexType rewrittenType = typeMap.getOrDefault(fieldType, fieldType);
-      if (rewrittenHolderType != holderType || rewrittenType != fieldType) {
-        return instruction.createWithField(
-            factory.createField(rewrittenHolderType, rewrittenType, instruction.getField().name));
-      }
-      return instruction;
-    }
-
-    private CfInstruction rewriteTypeInstruction(CfTypeInstruction instruction) {
-      DexType rewrittenType = typeMap.getOrDefault(instruction.getType(), instruction.getType());
-      return rewrittenType != instruction.getType() ? instruction.withType(rewrittenType) : null;
-    }
-
-    private CfInstruction rewriteFrameInstruction(CfFrame instruction) {
-      return instruction.asFrame().mapReferenceTypes(type -> typeMap.getOrDefault(type, type));
-    }
-  }
-
   @Override
   protected CfCode getCode(String holderName, String methodName, CfCode code) {
     if (methodName.endsWith("Stub")) {
@@ -178,6 +103,7 @@
     // sun.misc.Unsafe.
     InstructionTypeMapper instructionTypeMapper =
         new InstructionTypeMapper(
+            factory,
             ImmutableMap.of(
                 factory.createType(
                     DescriptorUtils.javaClassToDescriptor(DesugarMethodHandlesLookup.class)),
diff --git a/third_party/android_jar/lib-v34.tar.gz.sha1 b/third_party/android_jar/lib-v34.tar.gz.sha1
index 3b7b739..0c3c782 100644
--- a/third_party/android_jar/lib-v34.tar.gz.sha1
+++ b/third_party/android_jar/lib-v34.tar.gz.sha1
@@ -1 +1 @@
-ac28074fa7b977e03eb0692248e1edcee36a2c33
\ No newline at end of file
+0b366542a5c6536e93da29e5f13bd320df6e2e34
\ No newline at end of file
diff --git a/third_party/api_database/api_database.tar.gz.sha1 b/third_party/api_database/api_database.tar.gz.sha1
index 9883fe5..5749b41 100644
--- a/third_party/api_database/api_database.tar.gz.sha1
+++ b/third_party/api_database/api_database.tar.gz.sha1
@@ -1 +1 @@
-29b5c8dfdccf33e7a540d5de29476805a7d5c2f0
\ No newline at end of file
+9570931632e830d695b18bd83f69456c74196d6e
\ No newline at end of file
diff --git a/third_party/dependencies_new.tar.gz.sha1 b/third_party/dependencies_new.tar.gz.sha1
index 5d89edc..e7d9678 100644
--- a/third_party/dependencies_new.tar.gz.sha1
+++ b/third_party/dependencies_new.tar.gz.sha1
@@ -1 +1 @@
-37f0c05fb0b91effe962a2dde30818c6e5e3cb23
\ No newline at end of file
+197067233a81d8fa441a320c4cad287fa5a3eeb3
\ No newline at end of file
diff --git a/third_party/openjdk/desugar_jdk_libs_releases/2.0.3.tar.gz.sha1 b/third_party/openjdk/desugar_jdk_libs_releases/2.0.3.tar.gz.sha1
new file mode 100644
index 0000000..017f318
--- /dev/null
+++ b/third_party/openjdk/desugar_jdk_libs_releases/2.0.3.tar.gz.sha1
@@ -0,0 +1 @@
+82006808a828dcb64726f119ab93402281fa9f17
\ No newline at end of file
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
diff --git a/tools/create_local_maven_with_dependencies.py b/tools/create_local_maven_with_dependencies.py
index 00f10d8..d6c0dd7 100755
--- a/tools/create_local_maven_with_dependencies.py
+++ b/tools/create_local_maven_with_dependencies.py
@@ -32,7 +32,10 @@
 ERROR_PRONE_VERSION = '2.18.0'
 TESTNG_VERSION = '6.10'
 
-
+# Resource shrinker dependency versions
+AAPT2_PROTO_VERSION = '8.2.0-alpha10-10154469'
+PROTOBUF_VERSION = '3.19.3'
+STUDIO_SDK_VERSION = '31.2.0-alpha10'
 
 BUILD_DEPENDENCIES = [
   'com.google.code.gson:gson:{version}'.format(version = GSON_VERSION),
@@ -65,7 +68,14 @@
   'org.jetbrains.kotlin:kotlin-reflect:1.8.10',
   'org.jetbrains.kotlin:kotlin-script-runtime:1.8.10',
   'org.jetbrains.kotlin:kotlin-tooling-core:1.8.10',
-  'net.ltgt.errorprone:net.ltgt.errorprone.gradle.plugin:3.0.1'
+  'net.ltgt.errorprone:net.ltgt.errorprone.gradle.plugin:3.0.1',
+
+  # Resource shrinker
+  'com.android.tools.build:aapt2-proto:{version}'.format(version = AAPT2_PROTO_VERSION),
+  'com.android.tools.layoutlib:layoutlib-api:{version}'.format(version = STUDIO_SDK_VERSION),
+  'com.android.tools:common:{version}'.format(version = STUDIO_SDK_VERSION),
+  'com.android.tools:sdk-common:{version}'.format(version = STUDIO_SDK_VERSION),
+  'com.google.protobuf:protobuf-java:{version}'.format(version = PROTOBUF_VERSION),
 ]
 
 def dependencies_tar(dependencies_path):
diff --git a/tools/linux/README.art-versions b/tools/linux/README.art-versions
index 03ddaa3..f813085 100644
--- a/tools/linux/README.art-versions
+++ b/tools/linux/README.art-versions
@@ -69,9 +69,9 @@
 
 art-14 (Android U)
 ------------------
-Build branch udc-preview1-release.
+Build branch udc-beta3-release. Art at 1f3514c28caf4537fe0fdf14559991ca23cf9e30.
 
-export BRANCH=udc-preview1-release
+export BRANCH=udc-beta3-release
 mkdir ${BRANCH}
 cd ${BRANCH}
 export ANDROID_CHECKOUT=$(pwd)
@@ -90,10 +90,10 @@
   cd <r8 checkout>
   scripts/update-host-art.sh \
      --android-checkout $ANDROID_CHECKOUT \
-     --art-dir host/art-14.0.0-dp1 \
+     --art-dir host/art-14.0.0-beta3 \
      --android-product redfin
 
-  (cd tools/linux/host; upload_to_google_storage.py -a --bucket r8-deps art-14.0.0-dp1)
+  (cd tools/linux/host; upload_to_google_storage.py -a --bucket r8-deps art-14.0.0-beta3)
 
 
 art-13 (Android T)
diff --git a/tools/linux/host/art-14.0.0-beta3.tar.gz.sha1 b/tools/linux/host/art-14.0.0-beta3.tar.gz.sha1
new file mode 100644
index 0000000..42f9f43
--- /dev/null
+++ b/tools/linux/host/art-14.0.0-beta3.tar.gz.sha1
@@ -0,0 +1 @@
+c879ddf6410159a4ac7e673fd66a64abe2140ffa
\ No newline at end of file
diff --git a/tools/linux/host/art-14.0.0-dp1.tar.gz.sha1 b/tools/linux/host/art-14.0.0-dp1.tar.gz.sha1
deleted file mode 100644
index c88b923..0000000
--- a/tools/linux/host/art-14.0.0-dp1.tar.gz.sha1
+++ /dev/null
@@ -1 +0,0 @@
-1c41f5d79e335ff132b7a95d995b65581c250df8
\ No newline at end of file