Merge commit '1c67df7e75812c0da636efa56997b3ef69afb798' into dev-release

Change-Id: I983dec38b0c8225b54ff2f0250a1b883cbab4be8
diff --git a/d8_r8/build.gradle.kts b/d8_r8/build.gradle.kts
index 850a0c5..187f708 100644
--- a/d8_r8/build.gradle.kts
+++ b/d8_r8/build.gradle.kts
@@ -19,4 +19,8 @@
   val r8 by registering() {
     dependsOn(gradle.includedBuild("main").task(":r8WithRelocatedDeps"))
   }
+
+  val r8lib by registering() {
+    dependsOn(gradle.includedBuild("test").task(":assembleR8LibWithRelocatedDeps"))
+  }
 }
diff --git a/d8_r8/commonBuildSrc/src/main/kotlin/DependenciesPlugin.kt b/d8_r8/commonBuildSrc/src/main/kotlin/DependenciesPlugin.kt
index 1ec0e8b..418e0d5 100644
--- a/d8_r8/commonBuildSrc/src/main/kotlin/DependenciesPlugin.kt
+++ b/d8_r8/commonBuildSrc/src/main/kotlin/DependenciesPlugin.kt
@@ -251,7 +251,7 @@
     osFolder = "windows"
   }
   if (os.isMacOsX) {
-    osFolder = "osx"
+    osFolder = "osx/Contents/Home"
   }
   return getRoot().resolveAll("third_party", "openjdk", jdk.folder, osFolder)
 }
diff --git a/doc/keepanno-guide.md b/doc/keepanno-guide.md
new file mode 100644
index 0000000..9970ae4
--- /dev/null
+++ b/doc/keepanno-guide.md
@@ -0,0 +1,113 @@
+[comment]: <> (DO NOT EDIT - GENERATED FILE)
+[comment]: <> (Changes should be made in doc/keepanno-guide.template.md)
+
+# Guide to Keep Annotations
+
+## Disclaimer
+
+The annotation library described here is in development and considered to be in
+its prototype phase. As such it is not yet feature complete, but we are actively
+working on supporting all of the use cases we know of. Once the design exits the
+prototype phase, it is intended to move to an R8 independent library as part of
+androidx. All feedback: criticism, comments and suggestions are very welcome!
+
+[File new feature requests and
+bugs](https://issuetracker.google.com/issues/new?component=326788) in the
+[R8 component](https://issuetracker.google.com/issues?q=status:open%20componentid:326788).
+
+
+
+## Introduction
+
+When using a Java/Kotlin shrinker such as R8 or Proguard, developers must inform
+the shrinker about parts of the program that are used either externally from the
+program itself or internally via reflection and therefore must be kept.
+
+Traditionally these aspects would be kept by writing keep rules in a
+configuration file and passing that to the shrinker.
+
+The keep annotations described in this document represent an alternative method
+using Java annotations. The motivation for using these annotations is foremost
+to place the description of what to keep closer to the program point using
+reflective behavior. Doing so more directly connects the reflective code with
+the keep specification and makes it easier to maintain as the code develops. In
+addition, the annotations are defined independent from keep rules and have a
+hopefully more clear and direct meaning.
+
+
+## Build configuration
+
+To use the keep annotations your build must include the library of
+annotations. It is currently built as part of each R8 build and if used with R8,
+you should use the matching version. You can find all archived builds at:
+
+```
+https://storage.googleapis.com/r8-releases/raw/<version>/keepanno-annotations.jar
+```
+
+Thus you may obtain version `8.2.34` by running:
+
+```
+wget https://storage.googleapis.com/r8-releases/raw/8.2.34/keepanno-annotations.jar
+```
+
+You will then need to set the system property
+`com.android.tools.r8.enableKeepAnnotations` to instruct R8 to make use of the
+annotations when shrinking:
+
+```
+java -Dcom.android.tools.r8.enableKeepAnnotations=1 \
+  -cp r8.jar com.android.tools.r8.R8 \
+  # ... the rest of your R8 compilation command here ...
+```
+
+### Annotating code using reflection
+
+The keep annotation library defines a family of annotations depending on your
+use case. You should generally prefer [@UsesReflection](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/UsesReflection.html) where applicable.
+
+For example, if your program is reflectively invoking a method, you
+should annotate the method that is doing the reflection. The annotation must describe the
+assumptions the reflective code makes.
+
+In the following example, the method `foo` is looking up the method with the name
+`hiddenMethod` on objects that are instances of `BaseClass`. It is then invoking the method with
+no other arguments than the receiver.
+
+The assumptions the code makes are that all methods with the name
+`hiddenMethod` and the empty list of parameters must remain valid for `getDeclaredMethod` if they
+are objects that are instances of the class `BaseClass` or subclasses thereof.
+
+
+```
+static class MyClass {
+
+  @UsesReflection({
+    @KeepTarget(
+        instanceOfClassConstant = BaseClass.class,
+        methodName = "hiddenMethod",
+        methodParameters = {})
+  })
+  public void foo(BaseClass base) throws Exception {
+    base.getClass().getDeclaredMethod("hiddenMethod").invoke(base);
+  }
+}
+```
+
+
+
+
+### Annotating code used by reflection (or via JNI)
+
+
+
+### Annotating APIs
+
+
+### Migrating rules to annotations
+
+
+### My use case is not covered!
+
+
+### Troubleshooting
diff --git a/doc/keepanno-guide.template.md b/doc/keepanno-guide.template.md
new file mode 100644
index 0000000..6e0a8df
--- /dev/null
+++ b/doc/keepanno-guide.template.md
@@ -0,0 +1,85 @@
+# Guide to Keep Annotations
+
+## Disclaimer
+
+The annotation library described here is in development and considered to be in
+its prototype phase. As such it is not yet feature complete, but we are actively
+working on supporting all of the use cases we know of. Once the design exits the
+prototype phase, it is intended to move to an R8 independent library as part of
+androidx. All feedback: criticism, comments and suggestions are very welcome!
+
+[File new feature requests and
+bugs](https://issuetracker.google.com/issues/new?component=326788) in the
+[R8 component](https://issuetracker.google.com/issues?q=status:open%20componentid:326788).
+
+
+
+## Introduction
+
+When using a Java/Kotlin shrinker such as R8 or Proguard, developers must inform
+the shrinker about parts of the program that are used either externally from the
+program itself or internally via reflection and therefore must be kept.
+
+Traditionally these aspects would be kept by writing keep rules in a
+configuration file and passing that to the shrinker.
+
+The keep annotations described in this document represent an alternative method
+using Java annotations. The motivation for using these annotations is foremost
+to place the description of what to keep closer to the program point using
+reflective behavior. Doing so more directly connects the reflective code with
+the keep specification and makes it easier to maintain as the code develops. In
+addition, the annotations are defined independent from keep rules and have a
+hopefully more clear and direct meaning.
+
+
+## Build configuration
+
+To use the keep annotations your build must include the library of
+annotations. It is currently built as part of each R8 build and if used with R8,
+you should use the matching version. You can find all archived builds at:
+
+```
+https://storage.googleapis.com/r8-releases/raw/<version>/keepanno-annotations.jar
+```
+
+Thus you may obtain version `8.2.34` by running:
+
+```
+wget https://storage.googleapis.com/r8-releases/raw/8.2.34/keepanno-annotations.jar
+```
+
+You will then need to set the system property
+`com.android.tools.r8.enableKeepAnnotations` to instruct R8 to make use of the
+annotations when shrinking:
+
+```
+java -Dcom.android.tools.r8.enableKeepAnnotations=1 \
+  -cp r8.jar com.android.tools.r8.R8 \
+  # ... the rest of your R8 compilation command here ...
+```
+
+### Annotating code using reflection
+
+The keep annotation library defines a family of annotations depending on your
+use case. You should generally prefer `@UsesReflection` where applicable.
+
+[[[INCLUDE DOC:UsesReflectionOnVirtualMethod]]]
+
+[[[INCLUDE CODE:UsesReflectionOnVirtualMethod]]]
+
+
+
+### Annotating code used by reflection (or via JNI)
+
+
+
+### Annotating APIs
+
+
+### Migrating rules to annotations
+
+
+### My use case is not covered!
+
+
+### Troubleshooting
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepBinding.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepBinding.java
index 9657a22..e97d2b5 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepBinding.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepBinding.java
@@ -31,7 +31,11 @@
 @Retention(RetentionPolicy.CLASS)
 public @interface KeepBinding {
 
-  /** Name with which other bindings, conditions or targets can reference the bound item pattern. */
+  /**
+   * Name with which other bindings, conditions or targets can reference the bound item pattern.
+   *
+   * @return Name of the binding.
+   */
   String bindingName();
 
   /**
@@ -47,6 +51,8 @@
    *
    * <p>If unspecified the default for an item with no member patterns is ONLY_CLASS and if it does
    * have member patterns the default is ONLY_MEMBERS
+   *
+   * @return The kind for this pattern.
    */
   KeepItemKind kind() default KeepItemKind.DEFAULT;
 
@@ -67,6 +73,8 @@
    * </ul>
    *
    * <p>If none are specified the default is to match any class.
+   *
+   * @return The name of the binding that defines the class.
    */
   String classFromBinding() default "";
 
@@ -81,6 +89,8 @@
    * </ul>
    *
    * <p>If none are specified the default is to match any class name.
+   *
+   * @return The qualified class name that defines the class.
    */
   String className() default "";
 
@@ -95,6 +105,8 @@
    * </ul>
    *
    * <p>If none are specified the default is to match any class name.
+   *
+   * @return The class-constant that defines the class.
    */
   Class<?> classConstant() default Object.class;
 
@@ -113,6 +125,8 @@
    * </ul>
    *
    * <p>If none are specified the default is to match any class instance.
+   *
+   * @return The qualified class name that defines what instance-of the class must be.
    */
   String instanceOfClassName() default "";
 
@@ -134,6 +148,8 @@
    * </ul>
    *
    * <p>If none are specified the default is to match any class instance.
+   *
+   * @return The qualified class name that defines what instance-of the class must be.
    */
   String instanceOfClassNameExclusive() default "";
 
@@ -152,6 +168,8 @@
    * </ul>
    *
    * <p>If none are specified the default is to match any class instance.
+   *
+   * @return The class constant that defines what instance-of the class must be.
    */
   Class<?> instanceOfClassConstant() default Object.class;
 
@@ -173,6 +191,8 @@
    * </ul>
    *
    * <p>If none are specified the default is to match any class instance.
+   *
+   * @return The class constant that defines what instance-of the class must be.
    */
   Class<?> instanceOfClassConstantExclusive() default Object.class;
 
@@ -182,8 +202,6 @@
    * <p>The pattern is exclusive in that it does not match classes that are instances of the
    * pattern, but only those that are instances of classes that are subclasses of the pattern.
    *
-   * <p>This property is deprecated, use instanceOfClassName instead.
-   *
    * <p>Mutually exclusive with the following other properties defining instance-of:
    *
    * <ul>
@@ -196,7 +214,11 @@
    * </ul>
    *
    * <p>If none are specified the default is to match any class instance.
+   *
+   * @return The class name that defines what the class must extend.
+   * @deprecated This property is deprecated, use {@link #instanceOfClassName} instead.
    */
+  @Deprecated
   String extendsClassName() default "";
 
   /**
@@ -205,8 +227,6 @@
    * <p>The pattern is exclusive in that it does not match classes that are instances of the
    * pattern, but only those that are instances of classes that are subclasses of the pattern.
    *
-   * <p>This property is deprecated, use instanceOfClassConstant instead.
-   *
    * <p>Mutually exclusive with the following other properties defining instance-of:
    *
    * <ul>
@@ -219,7 +239,11 @@
    * </ul>
    *
    * <p>If none are specified the default is to match any class instance.
+   *
+   * @return The class constant that defines what the class must extend.
+   * @deprecated This property is deprecated, use {@link #instanceOfClassConstant} instead.
    */
+  @Deprecated
   Class<?> extendsClassConstant() default Object.class;
 
   /**
@@ -227,6 +251,8 @@
    *
    * <p>Mutually exclusive with all field and method properties as use restricts the match to both
    * types of members.
+   *
+   * @return The member access-flag constraints that must be met.
    */
   MemberAccessFlags[] memberAccess() default {};
 
@@ -237,6 +263,8 @@
    *
    * <p>If none, and other properties define this item as a method, the default matches any
    * method-access flags.
+   *
+   * @return The method access-flag constraints that must be met.
    */
   MethodAccessFlags[] methodAccess() default {};
 
@@ -247,6 +275,8 @@
    *
    * <p>If none, and other properties define this item as a method, the default matches any method
    * name.
+   *
+   * @return The exact method name of the method.
    */
   String methodName() default "";
 
@@ -257,6 +287,8 @@
    *
    * <p>If none, and other properties define this item as a method, the default matches any return
    * type.
+   *
+   * @return The qualified type name of the method return type.
    */
   String methodReturnType() default "";
 
@@ -267,6 +299,8 @@
    *
    * <p>If none, and other properties define this item as a method, the default matches any
    * parameters.
+   *
+   * @return The list of qualified type names of the method parameters.
    */
   String[] methodParameters() default {"<default>"};
 
@@ -277,6 +311,8 @@
    *
    * <p>If none, and other properties define this item as a field, the default matches any
    * field-access flags.
+   *
+   * @return The field access-flag constraints that must be met.
    */
   FieldAccessFlags[] fieldAccess() default {};
 
@@ -287,6 +323,8 @@
    *
    * <p>If none, and other properties define this item as a field, the default matches any field
    * name.
+   *
+   * @return The exact field name of the field.
    */
   String fieldName() default "";
 
@@ -296,6 +334,8 @@
    * <p>Mutually exclusive with all method properties.
    *
    * <p>If none, and other properties define this item as a field, the default matches any type.
+   *
+   * @return The qualified type name of the field type.
    */
   String fieldType() default "";
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepCondition.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepCondition.java
index 7777fd8..e539cc4 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepCondition.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepCondition.java
@@ -45,6 +45,8 @@
    * </ul>
    *
    * <p>If none are specified the default is to match any class.
+   *
+   * @return The name of the binding that defines the class.
    */
   String classFromBinding() default "";
 
@@ -59,6 +61,8 @@
    * </ul>
    *
    * <p>If none are specified the default is to match any class name.
+   *
+   * @return The qualified class name that defines the class.
    */
   String className() default "";
 
@@ -73,6 +77,8 @@
    * </ul>
    *
    * <p>If none are specified the default is to match any class name.
+   *
+   * @return The class-constant that defines the class.
    */
   Class<?> classConstant() default Object.class;
 
@@ -91,6 +97,8 @@
    * </ul>
    *
    * <p>If none are specified the default is to match any class instance.
+   *
+   * @return The qualified class name that defines what instance-of the class must be.
    */
   String instanceOfClassName() default "";
 
@@ -112,6 +120,8 @@
    * </ul>
    *
    * <p>If none are specified the default is to match any class instance.
+   *
+   * @return The qualified class name that defines what instance-of the class must be.
    */
   String instanceOfClassNameExclusive() default "";
 
@@ -130,6 +140,8 @@
    * </ul>
    *
    * <p>If none are specified the default is to match any class instance.
+   *
+   * @return The class constant that defines what instance-of the class must be.
    */
   Class<?> instanceOfClassConstant() default Object.class;
 
@@ -151,6 +163,8 @@
    * </ul>
    *
    * <p>If none are specified the default is to match any class instance.
+   *
+   * @return The class constant that defines what instance-of the class must be.
    */
   Class<?> instanceOfClassConstantExclusive() default Object.class;
 
@@ -160,8 +174,6 @@
    * <p>The pattern is exclusive in that it does not match classes that are instances of the
    * pattern, but only those that are instances of classes that are subclasses of the pattern.
    *
-   * <p>This property is deprecated, use instanceOfClassName instead.
-   *
    * <p>Mutually exclusive with the following other properties defining instance-of:
    *
    * <ul>
@@ -174,7 +186,11 @@
    * </ul>
    *
    * <p>If none are specified the default is to match any class instance.
+   *
+   * @return The class name that defines what the class must extend.
+   * @deprecated This property is deprecated, use {@link #instanceOfClassName} instead.
    */
+  @Deprecated
   String extendsClassName() default "";
 
   /**
@@ -183,8 +199,6 @@
    * <p>The pattern is exclusive in that it does not match classes that are instances of the
    * pattern, but only those that are instances of classes that are subclasses of the pattern.
    *
-   * <p>This property is deprecated, use instanceOfClassConstant instead.
-   *
    * <p>Mutually exclusive with the following other properties defining instance-of:
    *
    * <ul>
@@ -197,7 +211,11 @@
    * </ul>
    *
    * <p>If none are specified the default is to match any class instance.
+   *
+   * @return The class constant that defines what the class must extend.
+   * @deprecated This property is deprecated, use {@link #instanceOfClassConstant} instead.
    */
+  @Deprecated
   Class<?> extendsClassConstant() default Object.class;
 
   /**
@@ -205,6 +223,8 @@
    *
    * <p>Mutually exclusive with all other class and member pattern properties. When a member binding
    * is referenced this item is defined to be that item, including its class and member patterns.
+   *
+   * @return The binding name that defines the member.
    */
   String memberFromBinding() default "";
 
@@ -213,6 +233,8 @@
    *
    * <p>Mutually exclusive with all field and method properties as use restricts the match to both
    * types of members.
+   *
+   * @return The member access-flag constraints that must be met.
    */
   MemberAccessFlags[] memberAccess() default {};
 
@@ -223,6 +245,8 @@
    *
    * <p>If none, and other properties define this item as a method, the default matches any
    * method-access flags.
+   *
+   * @return The method access-flag constraints that must be met.
    */
   MethodAccessFlags[] methodAccess() default {};
 
@@ -233,6 +257,8 @@
    *
    * <p>If none, and other properties define this item as a method, the default matches any method
    * name.
+   *
+   * @return The exact method name of the method.
    */
   String methodName() default "";
 
@@ -243,6 +269,8 @@
    *
    * <p>If none, and other properties define this item as a method, the default matches any return
    * type.
+   *
+   * @return The qualified type name of the method return type.
    */
   String methodReturnType() default "";
 
@@ -253,6 +281,8 @@
    *
    * <p>If none, and other properties define this item as a method, the default matches any
    * parameters.
+   *
+   * @return The list of qualified type names of the method parameters.
    */
   String[] methodParameters() default {"<default>"};
 
@@ -263,6 +293,8 @@
    *
    * <p>If none, and other properties define this item as a field, the default matches any
    * field-access flags.
+   *
+   * @return The field access-flag constraints that must be met.
    */
   FieldAccessFlags[] fieldAccess() default {};
 
@@ -273,6 +305,8 @@
    *
    * <p>If none, and other properties define this item as a field, the default matches any field
    * name.
+   *
+   * @return The exact field name of the field.
    */
   String fieldName() default "";
 
@@ -282,6 +316,8 @@
    * <p>Mutually exclusive with all method properties.
    *
    * <p>If none, and other properties define this item as a field, the default matches any type.
+   *
+   * @return The qualified type name of the field type.
    */
   String fieldType() default "";
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepForApi.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepForApi.java
index 65454fe..cd3559c 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepForApi.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepForApi.java
@@ -27,13 +27,17 @@
 @Retention(RetentionPolicy.CLASS)
 public @interface KeepForApi {
 
-  /** Optional description to document the reason for this annotation. */
+  /**
+   * Optional description to document the reason for this annotation.
+   *
+   * @return The descriptive message. Defaults to no description.
+   */
   String description() default "";
 
   /**
    * Additional targets to be kept as part of the API surface.
    *
-   * <p>Defaults to no additional targets.
+   * @return List of additional target consequences. Defaults to no additional target consequences.
    */
   KeepTarget[] additionalTargets() default {};
 
@@ -46,6 +50,8 @@
    *
    * <p>It is not possible to use ONLY_CLASS if annotating a member. Also, it is never valid to use
    * kind ONLY_MEMBERS as the API surface must keep the class if any member is to be accessible.
+   *
+   * @return The kind for this pattern.
    */
   KeepItemKind kind() default KeepItemKind.DEFAULT;
 
@@ -54,6 +60,8 @@
    *
    * <p>Mutually exclusive with all field and method properties as use restricts the match to both
    * types of members.
+   *
+   * @return The member access-flag constraints that must be met.
    */
   MemberAccessFlags[] memberAccess() default {};
 
@@ -64,6 +72,8 @@
    *
    * <p>If none, and other properties define this item as a method, the default matches any
    * method-access flags.
+   *
+   * @return The method access-flag constraints that must be met.
    */
   MethodAccessFlags[] methodAccess() default {};
 
@@ -74,6 +84,8 @@
    *
    * <p>If none, and other properties define this item as a method, the default matches any method
    * name.
+   *
+   * @return The exact method name of the method.
    */
   String methodName() default "";
 
@@ -84,6 +96,8 @@
    *
    * <p>If none, and other properties define this item as a method, the default matches any return
    * type.
+   *
+   * @return The qualified type name of the method return type.
    */
   String methodReturnType() default "";
 
@@ -94,6 +108,8 @@
    *
    * <p>If none, and other properties define this item as a method, the default matches any
    * parameters.
+   *
+   * @return The list of qualified type names of the method parameters.
    */
   String[] methodParameters() default {"<default>"};
 
@@ -104,6 +120,8 @@
    *
    * <p>If none, and other properties define this item as a field, the default matches any
    * field-access flags.
+   *
+   * @return The field access-flag constraints that must be met.
    */
   FieldAccessFlags[] fieldAccess() default {};
 
@@ -114,6 +132,8 @@
    *
    * <p>If none, and other properties define this item as a field, the default matches any field
    * name.
+   *
+   * @return The exact field name of the field.
    */
   String fieldName() default "";
 
@@ -123,6 +143,8 @@
    * <p>Mutually exclusive with all method properties.
    *
    * <p>If none, and other properties define this item as a field, the default matches any type.
+   *
+   * @return The qualified type name of the field type.
    */
   String fieldType() default "";
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java
index 59e7e7c..8ad5098 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java
@@ -41,24 +41,34 @@
    *
    * <p>If unspecified the default for an item with no member patterns is ONLY_CLASS and if it does
    * have member patterns the default is ONLY_MEMBERS
+   *
+   * @return The kind for this pattern.
    */
   KeepItemKind kind() default KeepItemKind.DEFAULT;
 
   /**
-   * Define the options that do not need to be preserved for the target.
+   * Define the options that are allowed to be modified.
+   *
+   * <p>The specified options do not need to be preserved for the target.
    *
    * <p>Mutually exclusive with the property `disallow` also defining options.
    *
    * <p>If nothing is specified for options the default is "allow none" / "disallow all".
+   *
+   * @return Options allowed to be modified for the target.
    */
   KeepOption[] allow() default {};
 
   /**
-   * Define the options that *must* be preserved for the target.
+   * Define the options that are not allowed to be modified.
+   *
+   * <p>The specified options *must* be preserved for the target.
    *
    * <p>Mutually exclusive with the property `allow` also defining options.
    *
    * <p>If nothing is specified for options the default is "allow none" / "disallow all".
+   *
+   * @return Options not allowed to be modified for the target.
    */
   KeepOption[] disallow() default {};
 
@@ -79,6 +89,8 @@
    * </ul>
    *
    * <p>If none are specified the default is to match any class.
+   *
+   * @return The name of the binding that defines the class.
    */
   String classFromBinding() default "";
 
@@ -93,6 +105,8 @@
    * </ul>
    *
    * <p>If none are specified the default is to match any class name.
+   *
+   * @return The qualified class name that defines the class.
    */
   String className() default "";
 
@@ -107,6 +121,8 @@
    * </ul>
    *
    * <p>If none are specified the default is to match any class name.
+   *
+   * @return The class-constant that defines the class.
    */
   Class<?> classConstant() default Object.class;
 
@@ -125,6 +141,8 @@
    * </ul>
    *
    * <p>If none are specified the default is to match any class instance.
+   *
+   * @return The qualified class name that defines what instance-of the class must be.
    */
   String instanceOfClassName() default "";
 
@@ -146,6 +164,8 @@
    * </ul>
    *
    * <p>If none are specified the default is to match any class instance.
+   *
+   * @return The qualified class name that defines what instance-of the class must be.
    */
   String instanceOfClassNameExclusive() default "";
 
@@ -164,6 +184,8 @@
    * </ul>
    *
    * <p>If none are specified the default is to match any class instance.
+   *
+   * @return The class constant that defines what instance-of the class must be.
    */
   Class<?> instanceOfClassConstant() default Object.class;
 
@@ -185,6 +207,8 @@
    * </ul>
    *
    * <p>If none are specified the default is to match any class instance.
+   *
+   * @return The class constant that defines what instance-of the class must be.
    */
   Class<?> instanceOfClassConstantExclusive() default Object.class;
 
@@ -194,8 +218,6 @@
    * <p>The pattern is exclusive in that it does not match classes that are instances of the
    * pattern, but only those that are instances of classes that are subclasses of the pattern.
    *
-   * <p>This property is deprecated, use instanceOfClassName instead.
-   *
    * <p>Mutually exclusive with the following other properties defining instance-of:
    *
    * <ul>
@@ -208,7 +230,11 @@
    * </ul>
    *
    * <p>If none are specified the default is to match any class instance.
+   *
+   * @return The class name that defines what the class must extend.
+   * @deprecated This property is deprecated, use {@link #instanceOfClassName} instead.
    */
+  @Deprecated
   String extendsClassName() default "";
 
   /**
@@ -217,8 +243,6 @@
    * <p>The pattern is exclusive in that it does not match classes that are instances of the
    * pattern, but only those that are instances of classes that are subclasses of the pattern.
    *
-   * <p>This property is deprecated, use instanceOfClassConstant instead.
-   *
    * <p>Mutually exclusive with the following other properties defining instance-of:
    *
    * <ul>
@@ -231,7 +255,11 @@
    * </ul>
    *
    * <p>If none are specified the default is to match any class instance.
+   *
+   * @return The class constant that defines what the class must extend.
+   * @deprecated This property is deprecated, use {@link #instanceOfClassConstant} instead.
    */
+  @Deprecated
   Class<?> extendsClassConstant() default Object.class;
 
   /**
@@ -239,6 +267,8 @@
    *
    * <p>Mutually exclusive with all other class and member pattern properties. When a member binding
    * is referenced this item is defined to be that item, including its class and member patterns.
+   *
+   * @return The binding name that defines the member.
    */
   String memberFromBinding() default "";
 
@@ -247,6 +277,8 @@
    *
    * <p>Mutually exclusive with all field and method properties as use restricts the match to both
    * types of members.
+   *
+   * @return The member access-flag constraints that must be met.
    */
   MemberAccessFlags[] memberAccess() default {};
 
@@ -257,6 +289,8 @@
    *
    * <p>If none, and other properties define this item as a method, the default matches any
    * method-access flags.
+   *
+   * @return The method access-flag constraints that must be met.
    */
   MethodAccessFlags[] methodAccess() default {};
 
@@ -267,6 +301,8 @@
    *
    * <p>If none, and other properties define this item as a method, the default matches any method
    * name.
+   *
+   * @return The exact method name of the method.
    */
   String methodName() default "";
 
@@ -277,6 +313,8 @@
    *
    * <p>If none, and other properties define this item as a method, the default matches any return
    * type.
+   *
+   * @return The qualified type name of the method return type.
    */
   String methodReturnType() default "";
 
@@ -287,6 +325,8 @@
    *
    * <p>If none, and other properties define this item as a method, the default matches any
    * parameters.
+   *
+   * @return The list of qualified type names of the method parameters.
    */
   String[] methodParameters() default {"<default>"};
 
@@ -297,6 +337,8 @@
    *
    * <p>If none, and other properties define this item as a field, the default matches any
    * field-access flags.
+   *
+   * @return The field access-flag constraints that must be met.
    */
   FieldAccessFlags[] fieldAccess() default {};
 
@@ -307,6 +349,8 @@
    *
    * <p>If none, and other properties define this item as a field, the default matches any field
    * name.
+   *
+   * @return The exact field name of the field.
    */
   String fieldName() default "";
 
@@ -316,6 +360,8 @@
    * <p>Mutually exclusive with all method properties.
    *
    * <p>If none, and other properties define this item as a field, the default matches any type.
+   *
+   * @return The qualified type name of the field type.
    */
   String fieldType() default "";
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByNative.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByNative.java
index b1ad804..beba152 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByNative.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByNative.java
@@ -32,20 +32,25 @@
 @Retention(RetentionPolicy.CLASS)
 public @interface UsedByNative {
 
-  /** Optional description to document the reason for this annotation. */
+  /**
+   * Optional description to document the reason for this annotation.
+   *
+   * @return The descriptive message. Defaults to no description.
+   */
   String description() default "";
 
   /**
    * Conditions that should be satisfied for the annotation to be in effect.
    *
-   * <p>Defaults to no conditions, thus trivially/unconditionally satisfied.
+   * @return The list of preconditions. Defaults to no conditions, thus trivially/unconditionally
+   *     satisfied.
    */
   KeepCondition[] preconditions() default {};
 
   /**
    * Additional targets to be kept in addition to the annotated class/members.
    *
-   * <p>Defaults to no additional targets.
+   * @return List of additional target consequences. Defaults to no additional target consequences.
    */
   KeepTarget[] additionalTargets() default {};
 
@@ -61,6 +66,8 @@
    * <p>When annotating a member, the default kind is {@link KeepItemKind#ONLY_MEMBERS}.
    *
    * <p>It is not possible to use ONLY_CLASS if annotating a member.
+   *
+   * @return The kind for this pattern.
    */
   KeepItemKind kind() default KeepItemKind.DEFAULT;
 
@@ -69,6 +76,8 @@
    *
    * <p>Mutually exclusive with all field and method properties as use restricts the match to both
    * types of members.
+   *
+   * @return The member access-flag constraints that must be met.
    */
   MemberAccessFlags[] memberAccess() default {};
 
@@ -79,6 +88,8 @@
    *
    * <p>If none, and other properties define this item as a method, the default matches any
    * method-access flags.
+   *
+   * @return The method access-flag constraints that must be met.
    */
   MethodAccessFlags[] methodAccess() default {};
 
@@ -89,6 +100,8 @@
    *
    * <p>If none, and other properties define this item as a method, the default matches any method
    * name.
+   *
+   * @return The exact method name of the method.
    */
   String methodName() default "";
 
@@ -99,6 +112,8 @@
    *
    * <p>If none, and other properties define this item as a method, the default matches any return
    * type.
+   *
+   * @return The qualified type name of the method return type.
    */
   String methodReturnType() default "";
 
@@ -109,6 +124,8 @@
    *
    * <p>If none, and other properties define this item as a method, the default matches any
    * parameters.
+   *
+   * @return The list of qualified type names of the method parameters.
    */
   String[] methodParameters() default {"<default>"};
 
@@ -119,6 +136,8 @@
    *
    * <p>If none, and other properties define this item as a field, the default matches any
    * field-access flags.
+   *
+   * @return The field access-flag constraints that must be met.
    */
   FieldAccessFlags[] fieldAccess() default {};
 
@@ -129,6 +148,8 @@
    *
    * <p>If none, and other properties define this item as a field, the default matches any field
    * name.
+   *
+   * @return The exact field name of the field.
    */
   String fieldName() default "";
 
@@ -138,6 +159,8 @@
    * <p>Mutually exclusive with all method properties.
    *
    * <p>If none, and other properties define this item as a field, the default matches any type.
+   *
+   * @return The qualified type name of the field type.
    */
   String fieldType() default "";
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByReflection.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByReflection.java
index 27489cb..8c6e153 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByReflection.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByReflection.java
@@ -32,20 +32,25 @@
 @Retention(RetentionPolicy.CLASS)
 public @interface UsedByReflection {
 
-  /** Optional description to document the reason for this annotation. */
+  /**
+   * Optional description to document the reason for this annotation.
+   *
+   * @return The descriptive message. Defaults to no description.
+   */
   String description() default "";
 
   /**
    * Conditions that should be satisfied for the annotation to be in effect.
    *
-   * <p>Defaults to no conditions, thus trivially/unconditionally satisfied.
+   * @return The list of preconditions. Defaults to no conditions, thus trivially/unconditionally
+   *     satisfied.
    */
   KeepCondition[] preconditions() default {};
 
   /**
    * Additional targets to be kept in addition to the annotated class/members.
    *
-   * <p>Defaults to no additional targets.
+   * @return List of additional target consequences. Defaults to no additional target consequences.
    */
   KeepTarget[] additionalTargets() default {};
 
@@ -61,6 +66,8 @@
    * <p>When annotating a member, the default kind is {@link KeepItemKind#ONLY_MEMBERS}.
    *
    * <p>It is not possible to use ONLY_CLASS if annotating a member.
+   *
+   * @return The kind for this pattern.
    */
   KeepItemKind kind() default KeepItemKind.DEFAULT;
 
@@ -69,6 +76,8 @@
    *
    * <p>Mutually exclusive with all field and method properties as use restricts the match to both
    * types of members.
+   *
+   * @return The member access-flag constraints that must be met.
    */
   MemberAccessFlags[] memberAccess() default {};
 
@@ -79,6 +88,8 @@
    *
    * <p>If none, and other properties define this item as a method, the default matches any
    * method-access flags.
+   *
+   * @return The method access-flag constraints that must be met.
    */
   MethodAccessFlags[] methodAccess() default {};
 
@@ -89,6 +100,8 @@
    *
    * <p>If none, and other properties define this item as a method, the default matches any method
    * name.
+   *
+   * @return The exact method name of the method.
    */
   String methodName() default "";
 
@@ -99,6 +112,8 @@
    *
    * <p>If none, and other properties define this item as a method, the default matches any return
    * type.
+   *
+   * @return The qualified type name of the method return type.
    */
   String methodReturnType() default "";
 
@@ -109,6 +124,8 @@
    *
    * <p>If none, and other properties define this item as a method, the default matches any
    * parameters.
+   *
+   * @return The list of qualified type names of the method parameters.
    */
   String[] methodParameters() default {"<default>"};
 
@@ -119,6 +136,8 @@
    *
    * <p>If none, and other properties define this item as a field, the default matches any
    * field-access flags.
+   *
+   * @return The field access-flag constraints that must be met.
    */
   FieldAccessFlags[] fieldAccess() default {};
 
@@ -129,6 +148,8 @@
    *
    * <p>If none, and other properties define this item as a field, the default matches any field
    * name.
+   *
+   * @return The exact field name of the field.
    */
   String fieldName() default "";
 
@@ -138,6 +159,8 @@
    * <p>Mutually exclusive with all method properties.
    *
    * <p>If none, and other properties define this item as a field, the default matches any type.
+   *
+   * @return The qualified type name of the field type.
    */
   String fieldType() default "";
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsesReflection.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsesReflection.java
index 56a215d..fd28b1a 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsesReflection.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsesReflection.java
@@ -63,16 +63,24 @@
 @Retention(RetentionPolicy.CLASS)
 public @interface UsesReflection {
 
-  /** Optional description to document the reason for this annotation. */
+  /**
+   * Optional description to document the reason for this annotation.
+   *
+   * @return The descriptive message. Defaults to no description.
+   */
   String description() default "";
 
-  /** Consequences that must be kept if the annotation is in effect. */
+  /**
+   * Consequences that must be kept if the annotation is in effect.
+   *
+   * @return The list of target consequences.
+   */
   KeepTarget[] value();
 
   /**
    * Additional preconditions for the annotation to be in effect.
    *
-   * <p>Defaults to no additional preconditions.
+   * @return The list of additional preconditions. Defaults to no additional preconditions.
    */
   KeepCondition[] additionalPreconditions() default {};
 }
diff --git a/src/main/java/com/android/tools/r8/ArchiveProtoAndroidResourceConsumer.java b/src/main/java/com/android/tools/r8/ArchiveProtoAndroidResourceConsumer.java
index 9bb7f82..eed5427 100644
--- a/src/main/java/com/android/tools/r8/ArchiveProtoAndroidResourceConsumer.java
+++ b/src/main/java/com/android/tools/r8/ArchiveProtoAndroidResourceConsumer.java
@@ -4,29 +4,67 @@
 package com.android.tools.r8;
 
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
+import com.android.tools.r8.origin.PathOrigin;
 import com.android.tools.r8.utils.ArchiveBuilder;
-import com.android.tools.r8.utils.OutputBuilder;
+import com.android.tools.r8.utils.ExceptionDiagnostic;
+import com.android.tools.r8.utils.ZipUtils;
+import java.io.IOException;
 import java.nio.file.Path;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.zip.ZipEntry;
 
 @KeepForApi
 public class ArchiveProtoAndroidResourceConsumer implements AndroidResourceConsumer {
-  private final OutputBuilder outputBuilder;
+  private final ArchiveBuilder archiveBuilder;
+  private final Path inputPath;
+  private Map<String, Boolean> compressionMap;
 
   public ArchiveProtoAndroidResourceConsumer(Path outputPath) {
-    this.outputBuilder = new ArchiveBuilder(outputPath);
-    this.outputBuilder.open();
+    this(outputPath, null);
+  }
+
+  public ArchiveProtoAndroidResourceConsumer(Path outputPath, Path inputPath) {
+    this.archiveBuilder = new ArchiveBuilder(outputPath);
+    this.archiveBuilder.open();
+    this.inputPath = inputPath;
+  }
+
+  private synchronized Map<String, Boolean> getCompressionMap(
+      DiagnosticsHandler diagnosticsHandler) {
+    if (compressionMap != null) {
+      return compressionMap;
+    }
+    if (inputPath != null) {
+      compressionMap = new HashMap<>();
+      try {
+        ZipUtils.iter(
+            inputPath,
+            entry -> {
+              compressionMap.put(entry.getName(), entry.getMethod() != ZipEntry.STORED);
+            });
+      } catch (IOException e) {
+        diagnosticsHandler.error(new ExceptionDiagnostic(e, new PathOrigin(inputPath)));
+      }
+    } else {
+      compressionMap = Collections.emptyMap();
+    }
+    return compressionMap;
   }
 
   @Override
   public void accept(AndroidResourceOutput androidResource, DiagnosticsHandler diagnosticsHandler) {
-    outputBuilder.addFile(
+    archiveBuilder.addFile(
         androidResource.getPath().location(),
         androidResource.getByteDataView(),
-        diagnosticsHandler);
+        diagnosticsHandler,
+        getCompressionMap(diagnosticsHandler)
+            .getOrDefault(androidResource.getPath().location(), true));
   }
 
   @Override
   public void finished(DiagnosticsHandler handler) {
-    outputBuilder.close(handler);
+    archiveBuilder.close(handler);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 6e8edf7..6923215 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -856,15 +856,15 @@
       assert appView.getDontWarnConfiguration().validate(options);
 
       options.printWarnings();
+
+      if (options.printTimes) {
+        timing.report();
+      }
     } catch (ExecutionException e) {
       throw unwrapExecutionException(e);
     } finally {
       inputApp.signalFinishedToProviders(options.reporter);
       options.signalFinishedToConsumers();
-      // Dump timings.
-      if (options.printTimes) {
-        timing.report();
-      }
     }
   }
 
@@ -906,8 +906,7 @@
       LegacyResourceShrinker shrinker = resourceShrinkerBuilder.build();
       ShrinkerResult shrinkerResult;
       if (options.resourceShrinkerConfiguration.isOptimizedShrinking()) {
-        shrinkerResult =
-            shrinker.shrinkModel(appView.getResourceShrinkerState().getR8ResourceShrinkerModel());
+        shrinkerResult = shrinker.shrinkModel(appView.getResourceAnalysisResult().getModel());
       } else {
         shrinkerResult = shrinker.run();
       }
diff --git a/src/main/java/com/android/tools/r8/graph/AppView.java b/src/main/java/com/android/tools/r8/graph/AppView.java
index 3e3fe4c..f806710 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -4,7 +4,6 @@
 
 package com.android.tools.r8.graph;
 
-import com.android.build.shrinker.r8integration.R8ResourceShrinkerState;
 import com.android.tools.r8.androidapi.AndroidApiLevelCompute;
 import com.android.tools.r8.androidapi.ComputedApiLevel;
 import com.android.tools.r8.contexts.CompilationContext;
@@ -13,6 +12,7 @@
 import com.android.tools.r8.features.ClassToFeatureSplitMap;
 import com.android.tools.r8.graph.DexValue.DexValueString;
 import com.android.tools.r8.graph.analysis.InitializedClassesInInstanceMethodsAnalysis.InitializedClassesInInstanceMethods;
+import com.android.tools.r8.graph.analysis.ResourceAccessAnalysis.ResourceAnalysisResult;
 import com.android.tools.r8.graph.classmerging.MergedClassesCollection;
 import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.graph.lens.InitClassLens;
@@ -149,7 +149,7 @@
 
   private SeedMapper applyMappingSeedMapper;
 
-  R8ResourceShrinkerState resourceShrinkerState = null;
+  private ResourceAnalysisResult resourceAnalysisResult = null;
 
   // When input has been (partially) desugared these are the classes which has been library
   // desugared. This information is populated in the IR converter.
@@ -871,12 +871,12 @@
     testing().unboxedEnumsConsumer.accept(dexItemFactory(), unboxedEnums);
   }
 
-  public R8ResourceShrinkerState getResourceShrinkerState() {
-    return resourceShrinkerState;
+  public void setResourceAnalysisResult(ResourceAnalysisResult resourceAnalysisResult) {
+    this.resourceAnalysisResult = resourceAnalysisResult;
   }
 
-  public void setResourceShrinkerState(R8ResourceShrinkerState resourceShrinkerState) {
-    this.resourceShrinkerState = resourceShrinkerState;
+  public ResourceAnalysisResult getResourceAnalysisResult() {
+    return resourceAnalysisResult;
   }
 
   public boolean validateUnboxedEnumsHaveBeenPruned() {
@@ -971,6 +971,9 @@
       setProguardCompatibilityActions(
           getProguardCompatibilityActions().withoutPrunedItems(prunedItems, timing));
     }
+    if (resourceAnalysisResult != null) {
+      resourceAnalysisResult.withoutPrunedItems(prunedItems, timing);
+    }
     if (hasRootSet()) {
       rootSet.pruneItems(prunedItems, timing);
     }
@@ -1175,6 +1178,17 @@
               new ThreadTask() {
                 @Override
                 public void run(Timing threadTiming) {
+                  appView.resourceAnalysisResult.rewrittenWithLens(lens, threadTiming);
+                }
+
+                @Override
+                public boolean shouldRun() {
+                  return appView.resourceAnalysisResult != null;
+                }
+              },
+              new ThreadTask() {
+                @Override
+                public void run(Timing threadTiming) {
                   appView.setRootSet(appView.rootSet().rewrittenWithLens(lens, threadTiming));
                 }
 
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 92ceda8..39c4168 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -93,6 +93,7 @@
   public static final String androidMediaMediaDrmDescriptorString = "Landroid/media/MediaDrm;";
   public static final String androidMediaMediaMetadataRetrieverDescriptorString =
       "Landroid/media/MediaMetadataRetriever;";
+  public static final String androidResourcesDescriptorString = "Landroid/content/res/Resources;";
 
   /** Set of types that may be synthesized during compilation. */
   private final Set<DexType> possibleCompilerSynthesizedTypes = Sets.newIdentityHashSet();
@@ -643,6 +644,13 @@
       createStaticallyKnownType(androidMediaMediaDrmDescriptorString);
   public final DexType androidMediaMediaMetadataRetrieverType =
       createStaticallyKnownType(androidMediaMediaMetadataRetrieverDescriptorString);
+  public final DexType androidResourcesType =
+      createStaticallyKnownType(androidResourcesDescriptorString);
+  public final DexString androidResourcesGetStringName = createString("getString");
+  public final DexProto androidResourcesGetStringProto = createProto(stringType, intType);
+  public final DexMethod androidResourcesGetStringMethod =
+      createMethod(
+          androidResourcesType, androidResourcesGetStringProto, androidResourcesGetStringName);
 
   public final StringBuildingMethods stringBuilderMethods =
       new StringBuildingMethods(stringBuilderType);
diff --git a/src/main/java/com/android/tools/r8/graph/analysis/ResourceAccessAnalysis.java b/src/main/java/com/android/tools/r8/graph/analysis/ResourceAccessAnalysis.java
index d4ebe2f..ed61adb 100644
--- a/src/main/java/com/android/tools/r8/graph/analysis/ResourceAccessAnalysis.java
+++ b/src/main/java/com/android/tools/r8/graph/analysis/ResourceAccessAnalysis.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.graph.analysis;
 
 import com.android.build.shrinker.r8integration.R8ResourceShrinkerState;
+import com.android.build.shrinker.r8integration.R8ResourceShrinkerState.R8ResourceShrinkerModel;
 import com.android.tools.r8.AndroidResourceInput;
 import com.android.tools.r8.AndroidResourceInput.Kind;
 import com.android.tools.r8.ResourceException;
@@ -12,9 +13,12 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.FieldResolutionResult.SingleFieldResolutionResult;
 import com.android.tools.r8.graph.ProgramField;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.PrunedItems;
+import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.NewArrayEmpty;
@@ -23,8 +27,10 @@
 import com.android.tools.r8.ir.conversion.MethodConversionOptions;
 import com.android.tools.r8.shaking.Enqueuer;
 import com.android.tools.r8.shaking.EnqueuerWorklist;
+import com.android.tools.r8.shaking.KeepFieldInfo;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.Timing;
 import it.unimi.dsi.fastutil.ints.IntArrayList;
 import it.unimi.dsi.fastutil.ints.IntList;
 import java.util.IdentityHashMap;
@@ -34,18 +40,15 @@
 public class ResourceAccessAnalysis implements EnqueuerFieldAccessAnalysis {
 
   private final R8ResourceShrinkerState resourceShrinkerState;
-  private final Map<DexProgramClass, RClassFieldToValueStore> fieldToValueMapping =
-      new IdentityHashMap<>();
+  private final Map<DexType, RClassFieldToValueStore> fieldToValueMapping = new IdentityHashMap<>();
   private final AppView<? extends AppInfoWithClassHierarchy> appView;
+  private final Enqueuer enqueuer;
 
-  @SuppressWarnings("UnusedVariable")
   private ResourceAccessAnalysis(
-      AppView<? extends AppInfoWithClassHierarchy> appView,
-      Enqueuer enqueuer,
-      R8ResourceShrinkerState resourceShrinkerState) {
+      AppView<? extends AppInfoWithClassHierarchy> appView, Enqueuer enqueuer) {
     this.appView = appView;
-    this.resourceShrinkerState = resourceShrinkerState;
-    appView.setResourceShrinkerState(resourceShrinkerState);
+    this.enqueuer = enqueuer;
+    this.resourceShrinkerState = new R8ResourceShrinkerState();
     try {
       for (AndroidResourceInput androidResource :
           appView.options().androidResourceProvider.getAndroidResources()) {
@@ -62,14 +65,14 @@
   public static void register(
       AppView<? extends AppInfoWithClassHierarchy> appView, Enqueuer enqueuer) {
     if (enabled(appView, enqueuer)) {
-      enqueuer.registerFieldAccessAnalysis(
-          new ResourceAccessAnalysis(appView, enqueuer, new R8ResourceShrinkerState()));
+      enqueuer.registerFieldAccessAnalysis(new ResourceAccessAnalysis(appView, enqueuer));
     }
   }
 
   @Override
   public void done(Enqueuer enqueuer) {
-    appView.setResourceShrinkerState(resourceShrinkerState);
+    appView.setResourceAnalysisResult(
+        new ResourceAnalysisResult(resourceShrinkerState, fieldToValueMapping));
     EnqueuerFieldAccessAnalysis.super.done(enqueuer);
   }
 
@@ -93,13 +96,15 @@
       return;
     }
     if (getMaybeCachedIsRClass(resolvedField.getHolder())) {
-      DexProgramClass holderType = resolvedField.getHolder();
+      DexType holderType = resolvedField.getHolderType();
       if (!fieldToValueMapping.containsKey(holderType)) {
         populateRClassValues(resolvedField);
       }
       assert fieldToValueMapping.containsKey(holderType);
       RClassFieldToValueStore rClassFieldToValueStore = fieldToValueMapping.get(holderType);
       IntList integers = rClassFieldToValueStore.valueMapping.get(field);
+      enqueuer.applyMinimumKeepInfoWhenLive(
+          resolvedField, KeepFieldInfo.newEmptyJoiner().disallowOptimization());
       for (Integer integer : integers) {
         resourceShrinkerState.trace(integer);
       }
@@ -161,7 +166,7 @@
       rClassValueBuilder.addMapping(staticPut.getField(), values);
     }
 
-    fieldToValueMapping.put(field.getHolder(), rClassValueBuilder.build());
+    fieldToValueMapping.put(field.getHolderType(), rClassValueBuilder.build());
   }
 
   private final Map<DexProgramClass, Boolean> cachedClassLookups = new IdentityHashMap<>();
@@ -189,13 +194,103 @@
     return isRClass;
   }
 
+  public static class ResourceAnalysisResult {
+
+    private final R8ResourceShrinkerState resourceShrinkerState;
+    private Map<DexType, RClassFieldToValueStore> rClassFieldToValueStoreMap;
+
+    private ResourceAnalysisResult(
+        R8ResourceShrinkerState resourceShrinkerState,
+        Map<DexType, RClassFieldToValueStore> rClassFieldToValueStoreMap) {
+      this.resourceShrinkerState = resourceShrinkerState;
+      this.rClassFieldToValueStoreMap = rClassFieldToValueStoreMap;
+    }
+
+    public R8ResourceShrinkerModel getModel() {
+      return resourceShrinkerState.getR8ResourceShrinkerModel();
+    }
+
+    @SuppressWarnings("ReferenceEquality")
+    public void rewrittenWithLens(GraphLens lens, Timing timing) {
+      Map<DexType, DexType> changed = new IdentityHashMap<>();
+      for (DexType dexType : rClassFieldToValueStoreMap.keySet()) {
+        DexType rewritten = lens.lookupClassType(dexType);
+        if (rewritten != dexType) {
+          changed.put(dexType, rewritten);
+        }
+      }
+      if (changed.size() > 0) {
+        Map<DexType, RClassFieldToValueStore> rewrittenMap = new IdentityHashMap<>();
+        rClassFieldToValueStoreMap.forEach(
+            (type, map) -> {
+              rewrittenMap.put(changed.getOrDefault(type, type), map);
+              map.rewrittenWithLens(lens);
+            });
+        rClassFieldToValueStoreMap = rewrittenMap;
+      }
+    }
+
+    public void withoutPrunedItems(PrunedItems prunedItems, Timing timing) {
+      rClassFieldToValueStoreMap.keySet().removeIf(prunedItems::isRemoved);
+      rClassFieldToValueStoreMap.values().forEach(store -> store.pruneItems(prunedItems));
+    }
+
+    public String getSingleStringValueForField(ProgramField programField) {
+      RClassFieldToValueStore rClassFieldToValueStore =
+          rClassFieldToValueStoreMap.get(programField.getHolderType());
+      if (rClassFieldToValueStore == null) {
+        return null;
+      }
+      if (!rClassFieldToValueStore.hasField(programField.getReference())) {
+        return null;
+      }
+      return getModel()
+          .getStringResourcesWithSingleValue()
+          .get(rClassFieldToValueStore.getResourceId(programField.getReference()));
+    }
+  }
+
   private static class RClassFieldToValueStore {
-    private final Map<DexField, IntList> valueMapping;
+    private Map<DexField, IntList> valueMapping;
 
     private RClassFieldToValueStore(Map<DexField, IntList> valueMapping) {
       this.valueMapping = valueMapping;
     }
 
+    boolean hasField(DexField field) {
+      return valueMapping.containsKey(field);
+    }
+
+    void pruneItems(PrunedItems prunedItems) {
+      valueMapping.keySet().removeIf(prunedItems::isRemoved);
+    }
+
+    int getResourceId(DexField field) {
+      IntList integers = valueMapping.get(field);
+      assert integers.size() == 1;
+      return integers.get(0);
+    }
+
+    @SuppressWarnings("ReferenceEquality")
+    public void rewrittenWithLens(GraphLens lens) {
+      Map<DexField, DexField> changed = new IdentityHashMap<>();
+      valueMapping
+          .keySet()
+          .forEach(
+              dexField -> {
+                DexField rewritten = lens.lookupField(dexField);
+                if (rewritten != dexField) {
+                  changed.put(dexField, rewritten);
+                }
+              });
+      if (changed.size() > 0) {
+        Map<DexField, IntList> rewrittenMapping = new IdentityHashMap<>();
+        valueMapping.forEach(
+            (key, value) -> rewrittenMapping.put(changed.getOrDefault(key, key), value));
+        valueMapping = rewrittenMapping;
+      }
+    }
+
     public static class Builder {
       private final Map<DexField, IntList> valueMapping = new IdentityHashMap<>();
 
diff --git a/src/main/java/com/android/tools/r8/graph/lens/GraphLens.java b/src/main/java/com/android/tools/r8/graph/lens/GraphLens.java
index b34bf05..64f8399 100644
--- a/src/main/java/com/android/tools/r8/graph/lens/GraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/lens/GraphLens.java
@@ -23,6 +23,7 @@
 import com.android.tools.r8.graph.proto.RewrittenPrototypeDescription;
 import com.android.tools.r8.ir.code.InvokeType;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
+import com.android.tools.r8.ir.optimize.CustomLensCodeRewriter;
 import com.android.tools.r8.ir.optimize.enums.EnumUnboxingLens;
 import com.android.tools.r8.optimize.MemberRebindingIdentityLens;
 import com.android.tools.r8.optimize.MemberRebindingLens;
@@ -384,10 +385,15 @@
     return true;
   }
 
-  public boolean hasCustomCodeRewritings() {
+  public boolean hasCustomLensCodeRewriter() {
     return false;
   }
 
+  public CustomLensCodeRewriter getCustomLensCodeRewriter() {
+    assert hasCustomLensCodeRewriter();
+    return null;
+  }
+
   public boolean isAppliedLens() {
     return false;
   }
diff --git a/src/main/java/com/android/tools/r8/graph/lens/NestedGraphLensWithCustomLensCodeRewriter.java b/src/main/java/com/android/tools/r8/graph/lens/NestedGraphLensWithCustomLensCodeRewriter.java
new file mode 100644
index 0000000..84599f0
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/lens/NestedGraphLensWithCustomLensCodeRewriter.java
@@ -0,0 +1,51 @@
+// 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.graph.lens;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.optimize.CustomLensCodeRewriter;
+import com.android.tools.r8.utils.collections.BidirectionalManyToManyRepresentativeMap;
+import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeMap;
+import java.util.Map;
+
+public class NestedGraphLensWithCustomLensCodeRewriter extends NestedGraphLens {
+
+  private CustomLensCodeRewriter customLensCodeRewriter;
+
+  public NestedGraphLensWithCustomLensCodeRewriter(
+      AppView<?> appView,
+      BidirectionalManyToOneRepresentativeMap<DexField, DexField> fieldMap,
+      BidirectionalManyToOneRepresentativeMap<DexMethod, DexMethod> methodMap,
+      BidirectionalManyToManyRepresentativeMap<DexType, DexType> typeMap) {
+    super(appView, fieldMap, methodMap, typeMap);
+  }
+
+  public NestedGraphLensWithCustomLensCodeRewriter(
+      AppView<?> appView,
+      BidirectionalManyToOneRepresentativeMap<DexField, DexField> fieldMap,
+      Map<DexMethod, DexMethod> methodMap,
+      BidirectionalManyToManyRepresentativeMap<DexType, DexType> typeMap,
+      BidirectionalManyToManyRepresentativeMap<DexMethod, DexMethod> newMethodSignatures) {
+    super(appView, fieldMap, methodMap, typeMap, newMethodSignatures);
+  }
+
+  @Override
+  public boolean hasCustomLensCodeRewriter() {
+    return true;
+  }
+
+  public void setCustomLensCodeRewriter(CustomLensCodeRewriter customLensCodeRewriter) {
+    this.customLensCodeRewriter = customLensCodeRewriter;
+  }
+
+  @Override
+  public CustomLensCodeRewriter getCustomLensCodeRewriter() {
+    assert customLensCodeRewriter != null;
+    return customLensCodeRewriter;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoResourceClasses.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoResourceClasses.java
index acf1bce..11a3951 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoResourceClasses.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoResourceClasses.java
@@ -11,7 +11,8 @@
 
   @Override
   public boolean canMerge(DexProgramClass program) {
-    return !program.getSimpleName().startsWith("R$");
+    String simpleName = program.getSimpleName();
+    return !simpleName.startsWith("R$") && !simpleName.contains("$R$");
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index 0e13017..2a10e4c 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -259,7 +259,7 @@
               : null;
       this.enumUnboxer = EnumUnboxer.create(appViewWithLiveness);
       this.numberUnboxer = NumberUnboxer.create(appViewWithLiveness);
-      this.lensCodeRewriter = new LensCodeRewriter(appViewWithLiveness, enumUnboxer);
+      this.lensCodeRewriter = new LensCodeRewriter(appViewWithLiveness);
       this.inliner = new Inliner(appViewWithLiveness, this, lensCodeRewriter);
       this.outliner = Outliner.create(appViewWithLiveness);
       this.memberValuePropagation = new R8MemberValuePropagation(appViewWithLiveness);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
index 9cb88c3..ad14d80 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
@@ -107,7 +107,7 @@
 import com.android.tools.r8.ir.code.ValueType;
 import com.android.tools.r8.ir.conversion.passes.TrivialPhiSimplifier;
 import com.android.tools.r8.ir.optimize.AffectedValues;
-import com.android.tools.r8.ir.optimize.enums.EnumUnboxer;
+import com.android.tools.r8.ir.optimize.CustomLensCodeRewriter;
 import com.android.tools.r8.optimize.MemberRebindingAnalysis;
 import com.android.tools.r8.optimize.argumentpropagation.lenscoderewriter.NullCheckInserter;
 import com.android.tools.r8.utils.ArrayUtils;
@@ -159,13 +159,11 @@
 
   private final AppView<? extends AppInfoWithClassHierarchy> appView;
   private final DexItemFactory factory;
-  private final EnumUnboxer enumUnboxer;
   private final InternalOptions options;
 
-  LensCodeRewriter(AppView<? extends AppInfoWithClassHierarchy> appView, EnumUnboxer enumUnboxer) {
+  LensCodeRewriter(AppView<? extends AppInfoWithClassHierarchy> appView) {
     this.appView = appView;
     this.factory = appView.dexItemFactory();
-    this.enumUnboxer = enumUnboxer;
     this.options = appView.options();
   }
 
@@ -227,10 +225,11 @@
     Set<UnusedArgument> unusedArguments = Sets.newIdentityHashSet();
     rewriteArguments(
         code, originalMethodReference, prototypeChanges, affectedPhis, unusedArguments);
-    if (graphLens.hasCustomCodeRewritings()) {
-      assert graphLens.isEnumUnboxerLens();
+    if (graphLens.hasCustomLensCodeRewriter()) {
       assert graphLens.getPrevious() == codeLens;
-      affectedPhis.addAll(enumUnboxer.rewriteCode(code, methodProcessor, prototypeChanges));
+      CustomLensCodeRewriter customLensCodeRewriter = graphLens.getCustomLensCodeRewriter();
+      affectedPhis.addAll(
+          customLensCodeRewriter.rewriteCode(code, methodProcessor, prototypeChanges, graphLens));
     }
     if (!unusedArguments.isEmpty()) {
       for (UnusedArgument unusedArgument : unusedArguments) {
@@ -1096,10 +1095,10 @@
       assert currentLens.isNonIdentityLens();
       NonIdentityGraphLens currentNonIdentityLens = currentLens.asNonIdentityLens();
       NonIdentityGraphLens fromInclusiveLens = currentNonIdentityLens;
-      if (!currentNonIdentityLens.hasCustomCodeRewritings()) {
+      if (!currentNonIdentityLens.hasCustomLensCodeRewriter()) {
         GraphLens fromInclusiveLensPredecessor = fromInclusiveLens.getPrevious();
         while (fromInclusiveLensPredecessor.isNonIdentityLens()
-            && !fromInclusiveLensPredecessor.hasCustomCodeRewritings()
+            && !fromInclusiveLensPredecessor.hasCustomLensCodeRewriter()
             && fromInclusiveLensPredecessor != codeLens) {
           fromInclusiveLens = fromInclusiveLensPredecessor.asNonIdentityLens();
           fromInclusiveLensPredecessor = fromInclusiveLens.getPrevious();
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/PrimaryR8IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/PrimaryR8IRConverter.java
index 6b6f2ad..8b4cbce 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/PrimaryR8IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/PrimaryR8IRConverter.java
@@ -212,8 +212,6 @@
 
     appView.clearMethodResolutionOptimizationInfoCollection();
 
-    enumUnboxer.unsetRewriter();
-
     // All the code that should be impacted by the lenses inserted between phase 1 and phase 2
     // have now been processed and rewritten, we clear code lens rewriting so that the class
     // staticizer and phase 3 does not perform again the rewriting.
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/apiconversion/DesugaredLibraryConversionCfProvider.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/apiconversion/DesugaredLibraryConversionCfProvider.java
index b1a71cf..e270daf 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/apiconversion/DesugaredLibraryConversionCfProvider.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/apiconversion/DesugaredLibraryConversionCfProvider.java
@@ -320,6 +320,20 @@
     cfInstructions.add(new CfInvoke(invoke.getOpcode(), convertedMethod, invoke.isInterface()));
 
     if (returnConversion != null) {
+      assert returnConversion.getArity() == 1 || returnConversion.getArity() == 2;
+      if (returnConversion.getArity() == 2) {
+        // If there is a second parameter, pass the receiver.
+        if (!invoke.isInvokeSuper(context.getHolderType())) {
+          appView
+              .reporter()
+              .error(
+                  "Cannot generate inlined api conversion for return type for "
+                      + invoke.getMethod()
+                      + " in "
+                      + context.getReference());
+        }
+        cfInstructions.add(new CfLoad(ValueType.OBJECT, 0));
+      }
       cfInstructions.add(new CfInvoke(Opcodes.INVOKESTATIC, returnConversion, false));
     }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/CustomLensCodeRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/CustomLensCodeRewriter.java
new file mode 100644
index 0000000..1f046d2
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/CustomLensCodeRewriter.java
@@ -0,0 +1,25 @@
+// 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.optimize;
+
+import com.android.tools.r8.graph.lens.NonIdentityGraphLens;
+import com.android.tools.r8.graph.proto.RewrittenPrototypeDescription;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Phi;
+import com.android.tools.r8.ir.conversion.MethodProcessor;
+import com.google.common.collect.Sets;
+import java.util.Set;
+
+public interface CustomLensCodeRewriter {
+
+  CustomLensCodeRewriter EMPTY =
+      (code, methodProcessor, prototypeChanges, lens) -> Sets.newIdentityHashSet();
+
+  Set<Phi> rewriteCode(
+      IRCode code,
+      MethodProcessor methodProcessor,
+      RewrittenPrototypeDescription prototypeChanges,
+      NonIdentityGraphLens lens);
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EmptyEnumUnboxer.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EmptyEnumUnboxer.java
index cc90f9b..4e07fba 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EmptyEnumUnboxer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EmptyEnumUnboxer.java
@@ -8,18 +8,14 @@
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.lens.GraphLens;
-import com.android.tools.r8.graph.proto.RewrittenPrototypeDescription;
 import com.android.tools.r8.ir.analysis.fieldvalueanalysis.StaticFieldValues;
 import com.android.tools.r8.ir.code.IRCode;
-import com.android.tools.r8.ir.code.Phi;
 import com.android.tools.r8.ir.conversion.IRConverter;
 import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.ir.conversion.PostMethodProcessor.Builder;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackDelayed;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.Timing;
-import java.util.Collections;
-import java.util.Set;
 import java.util.concurrent.ExecutorService;
 
 public class EmptyEnumUnboxer extends EnumUnboxer {
@@ -58,14 +54,6 @@
   }
 
   @Override
-  public Set<Phi> rewriteCode(
-      IRCode code,
-      MethodProcessor methodProcessor,
-      RewrittenPrototypeDescription prototypeChanges) {
-    return Collections.emptySet();
-  }
-
-  @Override
   @SuppressWarnings("BadImport")
   public void rewriteWithLens() {
     // Intentionally empty.
@@ -84,11 +72,6 @@
   }
 
   @Override
-  public void unsetRewriter() {
-    // Intentionally empty.
-  }
-
-  @Override
   public void updateEnumUnboxingCandidatesInfo() {
     // Intentionally empty.
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java
index 48ae3b2..081d608 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java
@@ -8,17 +8,14 @@
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.lens.GraphLens;
-import com.android.tools.r8.graph.proto.RewrittenPrototypeDescription;
 import com.android.tools.r8.ir.analysis.fieldvalueanalysis.StaticFieldValues;
 import com.android.tools.r8.ir.code.IRCode;
-import com.android.tools.r8.ir.code.Phi;
 import com.android.tools.r8.ir.conversion.IRConverter;
 import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.ir.conversion.PostMethodProcessor.Builder;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackDelayed;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.Timing;
-import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 
@@ -44,9 +41,6 @@
 
   public abstract void recordEnumState(DexProgramClass clazz, StaticFieldValues staticFieldValues);
 
-  public abstract Set<Phi> rewriteCode(
-      IRCode code, MethodProcessor methodProcessor, RewrittenPrototypeDescription prototypeChanges);
-
   public abstract void rewriteWithLens();
 
   @SuppressWarnings("BadImport")
@@ -59,7 +53,5 @@
       Timing timing)
       throws ExecutionException;
 
-  public abstract void unsetRewriter();
-
   public abstract void updateEnumUnboxingCandidatesInfo();
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java
index 5821b3a..5132141 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java
@@ -166,9 +166,6 @@
       checkNotNullMethodsBuilder;
 
   private final DexClassAndField ordinalField;
-
-  private EnumUnboxingRewriter enumUnboxerRewriter;
-
   private final boolean debugLogEnabled;
   private final Map<DexType, List<Reason>> debugLogs;
 
@@ -764,13 +761,12 @@
 
     updateOptimizationInfos(executorService, feedback, treeFixerResult, previousLens);
 
-    enumUnboxerRewriter =
+    enumUnboxingLens.setCustomLensCodeRewriter(
         new EnumUnboxingRewriter(
             appView,
             treeFixerResult.getCheckNotNullToCheckNotZeroMapping(),
-            enumUnboxingLens,
             enumDataMap,
-            utilityClasses);
+            utilityClasses));
 
     // Ensure determinism of method-to-reprocess set.
     appView.testing().checkDeterminism(postMethodProcessorBuilder::dump);
@@ -1785,22 +1781,4 @@
     enumUnboxingCandidatesInfo.addPrunedMethod(method);
     methodsDependingOnLibraryModelisation.remove(method.getReference(), appView.graphLens());
   }
-
-  @Override
-  public Set<Phi> rewriteCode(
-      IRCode code,
-      MethodProcessor methodProcessor,
-      RewrittenPrototypeDescription prototypeChanges) {
-    // This has no effect during primary processing since the enumUnboxerRewriter is set
-    // in between primary and post processing.
-    if (enumUnboxerRewriter != null) {
-      return enumUnboxerRewriter.rewriteCode(code, methodProcessor, prototypeChanges);
-    }
-    return Sets.newIdentityHashSet();
-  }
-
-  @Override
-  public void unsetRewriter() {
-    enumUnboxerRewriter = null;
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingLens.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingLens.java
index 95e4ea6..37b6314 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingLens.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingLens.java
@@ -14,7 +14,7 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.graph.lens.MethodLookupResult;
-import com.android.tools.r8.graph.lens.NestedGraphLens;
+import com.android.tools.r8.graph.lens.NestedGraphLensWithCustomLensCodeRewriter;
 import com.android.tools.r8.graph.proto.ArgumentInfoCollection;
 import com.android.tools.r8.graph.proto.RewrittenPrototypeDescription;
 import com.android.tools.r8.graph.proto.RewrittenTypeInfo;
@@ -44,7 +44,7 @@
 import java.util.Map;
 import java.util.Set;
 
-public class EnumUnboxingLens extends NestedGraphLens {
+public class EnumUnboxingLens extends NestedGraphLensWithCustomLensCodeRewriter {
 
   private final AbstractValueFactory abstractValueFactory;
   private final Map<DexMethod, RewrittenPrototypeDescription> prototypeChangesPerMethod;
@@ -68,11 +68,6 @@
   }
 
   @Override
-  public boolean hasCustomCodeRewritings() {
-    return true;
-  }
-
-  @Override
   public boolean isEnumUnboxerLens() {
     return true;
   }
@@ -82,6 +77,10 @@
     return this;
   }
 
+  public EnumDataMap getUnboxedEnums() {
+    return unboxedEnums;
+  }
+
   @Override
   public boolean isContextFreeForMethods(GraphLens codeLens) {
     if (codeLens == this) {
@@ -383,7 +382,8 @@
           originalCheckNotNullMethodSignature, checkNotNullMethod.getReference());
     }
 
-    public EnumUnboxingLens build(AppView<?> appView, Set<DexMethod> dispatchMethods) {
+    public EnumUnboxingLens build(
+        AppView<AppInfoWithLiveness> appView, Set<DexMethod> dispatchMethods) {
       assert !typeMap.isEmpty();
       return new EnumUnboxingLens(
           appView,
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java
index 46184c4..da575cd 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.lens.NonIdentityGraphLens;
 import com.android.tools.r8.graph.proto.ArgumentInfo;
 import com.android.tools.r8.graph.proto.RewrittenPrototypeDescription;
 import com.android.tools.r8.graph.proto.RewrittenTypeInfo;
@@ -42,6 +43,7 @@
 import com.android.tools.r8.ir.code.StaticGet;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.conversion.MethodProcessor;
+import com.android.tools.r8.ir.optimize.CustomLensCodeRewriter;
 import com.android.tools.r8.ir.optimize.enums.EnumInstanceFieldData.EnumInstanceFieldKnownData;
 import com.android.tools.r8.ir.optimize.enums.classification.CheckNotNullEnumUnboxerMethodClassification;
 import com.android.tools.r8.ir.optimize.enums.classification.EnumUnboxerMethodClassification;
@@ -58,27 +60,24 @@
 import java.util.Map;
 import java.util.Set;
 
-public class EnumUnboxingRewriter {
+public class EnumUnboxingRewriter implements CustomLensCodeRewriter {
 
   private final AppView<AppInfoWithLiveness> appView;
   private final Map<DexMethod, DexMethod> checkNotNullToCheckNotZeroMapping;
   private final DexItemFactory factory;
   private final InternalOptions options;
   private final EnumDataMap unboxedEnumsData;
-  private final EnumUnboxingLens enumUnboxingLens;
   private final EnumUnboxingUtilityClasses utilityClasses;
 
   EnumUnboxingRewriter(
       AppView<AppInfoWithLiveness> appView,
       Map<DexMethod, DexMethod> checkNotNullToCheckNotZeroMapping,
-      EnumUnboxingLens enumUnboxingLens,
       EnumDataMap unboxedEnumsInstanceFieldData,
       EnumUnboxingUtilityClasses utilityClasses) {
     this.appView = appView;
-    this.checkNotNullToCheckNotZeroMapping = checkNotNullToCheckNotZeroMapping;
     this.factory = appView.dexItemFactory();
     this.options = appView.options();
-    this.enumUnboxingLens = enumUnboxingLens;
+    this.checkNotNullToCheckNotZeroMapping = checkNotNullToCheckNotZeroMapping;
     this.unboxedEnumsData = unboxedEnumsInstanceFieldData;
     this.utilityClasses = utilityClasses;
   }
@@ -146,15 +145,19 @@
     return convertedEnums;
   }
 
-  Set<Phi> rewriteCode(
+  @Override
+  public Set<Phi> rewriteCode(
       IRCode code,
       MethodProcessor methodProcessor,
-      RewrittenPrototypeDescription prototypeChanges) {
+      RewrittenPrototypeDescription prototypeChanges,
+      NonIdentityGraphLens graphLens) {
     // We should not process the enum methods, they will be removed and they may contain invalid
     // rewriting rules.
     if (unboxedEnumsData.isEmpty()) {
       return Sets.newIdentityHashSet();
     }
+    assert graphLens.isEnumUnboxerLens();
+    EnumUnboxingLens enumUnboxingLens = graphLens.asEnumUnboxerLens();
     assert code.isConsistentSSABeforeTypesAreCorrect(appView);
     EnumUnboxerMethodProcessorEventConsumer eventConsumer = methodProcessor.getEventConsumer();
     Set<Phi> affectedPhis = Sets.newIdentityHashSet();
@@ -192,7 +195,8 @@
               blocks,
               block,
               iterator,
-              instruction.asInvokeMethodWithReceiver());
+              instruction.asInvokeMethodWithReceiver(),
+              enumUnboxingLens);
         } else if (instruction.isNewArrayFilled()) {
           rewriteNewArrayFilled(instruction.asNewArrayFilled(), code, convertedEnums, iterator);
         } else if (instruction.isInvokeStatic()) {
@@ -379,7 +383,8 @@
       BasicBlockIterator blocks,
       BasicBlock block,
       InstructionListIterator iterator,
-      InvokeMethodWithReceiver invoke) {
+      InvokeMethodWithReceiver invoke,
+      EnumUnboxingLens enumUnboxingLens) {
     ProgramMethod context = code.context();
     // If the receiver is null, then the invoke is not rewritten even if the receiver is an
     // unboxed enum, but we end up with null.ordinal() or similar which has the correct behavior.
@@ -699,7 +704,7 @@
     }
   }
 
-  public void rewriteNullCheck(
+  private void rewriteNullCheck(
       InstructionListIterator iterator,
       InvokeMethod invoke,
       ProgramMethod context,
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingTreeFixer.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingTreeFixer.java
index 59be01e..35e4f74 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingTreeFixer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingTreeFixer.java
@@ -54,6 +54,7 @@
 import com.android.tools.r8.ir.conversion.MethodConversionOptions;
 import com.android.tools.r8.ir.conversion.MethodProcessorEventConsumer;
 import com.android.tools.r8.ir.conversion.OneTimeMethodProcessor;
+import com.android.tools.r8.ir.optimize.CustomLensCodeRewriter;
 import com.android.tools.r8.ir.optimize.enums.EnumDataMap.EnumData;
 import com.android.tools.r8.ir.optimize.enums.classification.CheckNotNullEnumUnboxerMethodClassification;
 import com.android.tools.r8.ir.optimize.enums.code.CheckNotZeroCode;
@@ -164,6 +165,8 @@
     converter.outliner.rewriteWithLens();
 
     // Create mapping from checkNotNull() to checkNotZero() methods.
+    // The customLensCodeRewriter has to be non null for the duplication but is effectively unused.
+    lens.setCustomLensCodeRewriter(CustomLensCodeRewriter.EMPTY);
     BiMap<DexMethod, DexMethod> checkNotNullToCheckNotZeroMapping =
         duplicateCheckNotNullMethods(converter, executorService);
 
@@ -1120,7 +1123,7 @@
       return methodsToProcess;
     }
 
-    EnumUnboxingLens getLens() {
+    public EnumUnboxingLens getLens() {
       return lens;
     }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMemberOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMemberOptimizer.java
index f47e771..1dc3ca2 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMemberOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMemberOptimizer.java
@@ -50,6 +50,10 @@
     register(new ObjectsMethodOptimizer(appView));
     register(new StringBuilderMethodOptimizer(appView));
     register(new StringMethodOptimizer(appView));
+    if (appView.enableWholeProgramOptimizations()
+        && appView.options().resourceShrinkerConfiguration.isOptimizedShrinking()) {
+      register(new ResourcesMemberOptimizer(appView));
+    }
     if (appView.enableWholeProgramOptimizations()) {
       // Subtyping is required to prove the enum class is a subtype of java.lang.Enum.
       register(new EnumMethodOptimizer(appView));
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/ResourcesMemberOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/ResourcesMemberOptimizer.java
new file mode 100644
index 0000000..1246516
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/ResourcesMemberOptimizer.java
@@ -0,0 +1,147 @@
+// 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.optimize.library;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexClassAndMethod;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.FieldResolutionResult;
+import com.android.tools.r8.graph.ProgramField;
+import com.android.tools.r8.graph.TopDownClassHierarchyTraversal;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.BasicBlockIterator;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.InvokeVirtual;
+import com.android.tools.r8.ir.optimize.AffectedValues;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+public class ResourcesMemberOptimizer extends StatelessLibraryMethodModelCollection {
+
+  private final AppView<?> appView;
+  private final DexItemFactory dexItemFactory;
+  private Optional<Boolean> allowStringInlining = Optional.empty();
+
+  ResourcesMemberOptimizer(AppView<?> appView) {
+    this.appView = appView;
+    this.dexItemFactory = appView.dexItemFactory();
+  }
+
+  @SuppressWarnings("ReferenceEquality")
+  private synchronized boolean allowInliningOfGetStringCalls() {
+    if (allowStringInlining.isPresent()) {
+      return allowStringInlining.get();
+    }
+    // TODO(b/312406163): Allow androidx classes that overwrite this, but don't change the value
+    // or have side effects.
+    allowStringInlining = Optional.of(true);
+    Map<DexClass, Boolean> cachedResults = new IdentityHashMap<>();
+    TopDownClassHierarchyTraversal.forProgramClasses(appView.withClassHierarchy())
+        .visit(
+            appView.appInfo().classes(),
+            clazz -> {
+              if (isResourcesSubtype(cachedResults, clazz)) {
+                DexEncodedMethod dexEncodedMethod =
+                    clazz.lookupMethod(
+                        dexItemFactory.androidResourcesGetStringProto,
+                        dexItemFactory.androidResourcesGetStringName);
+                if (dexEncodedMethod != null) {
+                  // TODO(b/312695444): Break out of traversal when supported.
+                  allowStringInlining = Optional.of(false);
+                }
+              }
+            });
+    return allowStringInlining.get();
+  }
+
+  @SuppressWarnings("ReferenceEquality")
+  private boolean isResourcesSubtype(Map<DexClass, Boolean> cachedLookups, DexClass dexClass) {
+    Boolean cachedValue = cachedLookups.get(dexClass);
+    if (cachedValue != null) {
+      return cachedValue;
+    }
+    if (dexClass.type == dexItemFactory.androidResourcesType) {
+      return true;
+    }
+    if (dexClass.type == dexItemFactory.objectType) {
+      return false;
+    }
+
+    if (dexClass.superType != null) {
+      DexClass superClass = appView.definitionFor(dexClass.superType);
+      if (superClass != null) {
+        boolean superIsResourcesSubtype = isResourcesSubtype(cachedLookups, superClass);
+        cachedLookups.put(dexClass, superIsResourcesSubtype);
+        return superIsResourcesSubtype;
+      }
+    }
+    cachedLookups.put(dexClass, false);
+    return false;
+  }
+
+  @Override
+  public DexType getType() {
+    return dexItemFactory.androidResourcesType;
+  }
+
+  @Override
+  public InstructionListIterator optimize(
+      IRCode code,
+      BasicBlockIterator blockIterator,
+      InstructionListIterator instructionIterator,
+      InvokeMethod invoke,
+      DexClassAndMethod singleTarget,
+      AffectedValues affectedValues,
+      Set<BasicBlock> blocksToRemove) {
+    if (allowInliningOfGetStringCalls()
+        && singleTarget
+            .getReference()
+            .isIdenticalTo(dexItemFactory.androidResourcesGetStringMethod)) {
+      maybeInlineGetString(code, instructionIterator, invoke, affectedValues);
+    }
+    return instructionIterator;
+  }
+
+  @SuppressWarnings("ReferenceEquality")
+  private void maybeInlineGetString(
+      IRCode code,
+      InstructionListIterator instructionIterator,
+      InvokeMethod invoke,
+      AffectedValues affectedValues) {
+    if (invoke.isInvokeVirtual()) {
+      InvokeVirtual invokeVirtual = invoke.asInvokeVirtual();
+      DexMethod invokedMethod = invokeVirtual.getInvokedMethod();
+      assert invokedMethod.isIdenticalTo(dexItemFactory.androidResourcesGetStringMethod);
+      assert invoke.inValues().size() == 2;
+      Instruction valueDefinition = invoke.getLastArgument().definition;
+      if (valueDefinition != null && valueDefinition.isStaticGet()) {
+        DexField field = valueDefinition.asStaticGet().getField();
+        FieldResolutionResult fieldResolutionResult =
+            appView.appInfo().resolveField(field, code.context());
+        ProgramField resolvedField = fieldResolutionResult.getProgramField();
+        if (resolvedField != null) {
+          String singleStringValueForField =
+              appView.getResourceAnalysisResult().getSingleStringValueForField(resolvedField);
+          if (singleStringValueForField != null) {
+            DexString value = dexItemFactory.createString(singleStringValueForField);
+            instructionIterator.replaceCurrentInstructionWithConstString(
+                appView, code, value, affectedValues);
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/StringMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/StringMethodOptimizer.java
index a43cbe6..15092b9 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/StringMethodOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/StringMethodOptimizer.java
@@ -34,6 +34,7 @@
 import com.android.tools.r8.ir.code.StaticGet;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.optimize.AffectedValues;
+import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ValueUtils;
 import com.android.tools.r8.utils.ValueUtils.ArrayValues;
 import com.google.common.collect.ImmutableMap;
@@ -54,7 +55,12 @@
   StringMethodOptimizer(AppView<?> appView) {
     this.appView = appView;
     this.dexItemFactory = appView.dexItemFactory();
-    this.enableStringFormatOptimizations = appView.options().enableStringFormatOptimization;
+    InternalOptions options = appView.options();
+    this.enableStringFormatOptimizations =
+        appView.enableWholeProgramOptimizations()
+            && !options.debug
+            && options.isOptimizing()
+            && options.isShrinking();
     this.valueOfToStringAppend =
         ImmutableMap.<DexMethod, DexMethod>builder()
             .put(
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index c8d9806..d1a2dda 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -3778,7 +3778,7 @@
     }
   }
 
-  private void applyMinimumKeepInfoWhenLive(
+  public void applyMinimumKeepInfoWhenLive(
       ProgramField field, KeepFieldInfo.Joiner minimumKeepInfo) {
     applyMinimumKeepInfoWhenLive(field, minimumKeepInfo, EnqueuerEvent.unconditional());
   }
diff --git a/src/main/java/com/android/tools/r8/threading/ThreadingModule.java b/src/main/java/com/android/tools/r8/threading/ThreadingModule.java
index 1525e70..7e374c4 100644
--- a/src/main/java/com/android/tools/r8/threading/ThreadingModule.java
+++ b/src/main/java/com/android/tools/r8/threading/ThreadingModule.java
@@ -10,8 +10,6 @@
 import com.android.tools.r8.keepanno.annotations.MemberAccessFlags;
 import com.android.tools.r8.keepanno.annotations.UsedByReflection;
 import com.android.tools.r8.keepanno.annotations.UsesReflection;
-import com.android.tools.r8.threading.providers.blocking.ThreadingModuleBlockingProvider;
-import com.android.tools.r8.threading.providers.singlethreaded.ThreadingModuleSingleThreadedProvider;
 import java.util.List;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
@@ -43,20 +41,20 @@
     // Splitting up the names to make reflective identification unlikely.
     // We explicitly don't want R8 to optimize out the reflective lookup.
     private static final String PACKAGE = "com.android.tools.r8.threading.providers";
-    private static final String[] IMPLEMENTATIONS = {
-      "blocking.ThreadingModuleBlockingProvider",
-      "singlethreaded.ThreadingModuleSingleThreadedProvider"
-    };
+    private static final String BLOCKING_PROVIDER = "blocking.ThreadingModuleBlockingProvider";
+    private static final String SINGLE_THREADED_PROVIDER =
+        "singlethreaded.ThreadingModuleSingleThreadedProvider";
+    private static final String[] IMPLEMENTATIONS = {BLOCKING_PROVIDER, SINGLE_THREADED_PROVIDER};
 
     @UsesReflection({
       @KeepTarget(
           kind = KeepItemKind.CLASS_AND_MEMBERS,
-          classConstant = ThreadingModuleBlockingProvider.class,
+          className = PACKAGE + "." + "blocking.ThreadingModuleBlockingProvider",
           methodName = "<init>",
           methodParameters = {}),
       @KeepTarget(
           kind = KeepItemKind.CLASS_AND_MEMBERS,
-          classConstant = ThreadingModuleSingleThreadedProvider.class,
+          className = PACKAGE + "." + "singlethreaded.ThreadingModuleSingleThreadedProvider",
           methodName = "<init>",
           methodParameters = {})
     })
diff --git a/src/main/java/com/android/tools/r8/utils/ArchiveBuilder.java b/src/main/java/com/android/tools/r8/utils/ArchiveBuilder.java
index a50dbb9..1ff1d22 100644
--- a/src/main/java/com/android/tools/r8/utils/ArchiveBuilder.java
+++ b/src/main/java/com/android/tools/r8/utils/ArchiveBuilder.java
@@ -73,7 +73,7 @@
         writeDirectoryNow(data.name, handler);
       } else {
         assert data.content != null;
-        writeFileNow(data.name, data.content, handler);
+        writeFileNow(data.name, data.content, handler, data.storeCompressed);
       }
     }
   }
@@ -135,9 +135,9 @@
       ByteDataView view = ByteDataView.of(ByteStreams.toByteArray(in));
       synchronized (this) {
         if (AndroidApiDataAccess.isApiDatabaseEntry(name)) {
-          writeFileNow(name, view, handler);
+          writeFileNow(name, view, handler, true);
         } else {
-          delayedWrites.add(DelayedData.createFile(name, view));
+          delayedWrites.add(DelayedData.createFile(name, view, true));
         }
       }
     } catch (IOException e) {
@@ -150,16 +150,25 @@
 
   @Override
   public synchronized void addFile(String name, ByteDataView content, DiagnosticsHandler handler) {
-    delayedWrites.add(DelayedData.createFile(name,  ByteDataView.of(content.copyByteData())));
+    addFile(name, content, handler, true);
   }
 
-  private void writeFileNow(String name, ByteDataView content, DiagnosticsHandler handler) {
+  public synchronized void addFile(
+      String name, ByteDataView content, DiagnosticsHandler handler, boolean storeCompressed) {
+    delayedWrites.add(
+        DelayedData.createFile(name, ByteDataView.of(content.copyByteData()), storeCompressed));
+  }
+
+  private void writeFileNow(
+      String name, ByteDataView content, DiagnosticsHandler handler, boolean compressed) {
     try {
       ZipUtils.writeToZipStream(
           getStream(),
           name,
           content,
-          AndroidApiDataAccess.isApiDatabaseEntry(name) ? ZipEntry.STORED : ZipEntry.DEFLATED);
+          AndroidApiDataAccess.isApiDatabaseEntry(name) || !compressed
+              ? ZipEntry.STORED
+              : ZipEntry.DEFLATED);
     } catch (IOException e) {
       handleIOException(e, handler);
     }
@@ -168,7 +177,7 @@
   private void writeNextIfAvailable(DiagnosticsHandler handler) {
     DelayedData data = delayedClassesDexFiles.remove(classesFileIndex);
     while (data != null) {
-      writeFileNow(data.name, data.content, handler);
+      writeFileNow(data.name, data.content, handler, data.storeCompressed);
       classesFileIndex++;
       data = delayedClassesDexFiles.remove(classesFileIndex);
     }
@@ -179,13 +188,13 @@
       int index, String name, ByteDataView content, DiagnosticsHandler handler) {
     if (index == classesFileIndex) {
       // Fast case, we got the file in order (or we only had one).
-      writeFileNow(name, content, handler);
+      writeFileNow(name, content, handler, true);
       classesFileIndex++;
       writeNextIfAvailable(handler);
     } else {
       // Data is released in the application writer, take a copy.
-      delayedClassesDexFiles.put(index,
-          new DelayedData(name, ByteDataView.of(content.copyByteData()), false));
+      delayedClassesDexFiles.put(
+          index, new DelayedData(name, ByteDataView.of(content.copyByteData()), false, true));
     }
   }
 
@@ -203,19 +212,23 @@
     public final String name;
     public final ByteDataView content;
     public final boolean isDirectory;
+    public final boolean storeCompressed;
 
-    public static DelayedData createFile(String name, ByteDataView content) {
-      return new DelayedData(name, content, false);
+    public static DelayedData createFile(
+        String name, ByteDataView content, boolean storeCompressed) {
+      return new DelayedData(name, content, false, storeCompressed);
     }
 
     public static DelayedData createDirectory(String name) {
-      return new DelayedData(name, null, true);
+      return new DelayedData(name, null, true, true);
     }
 
-    private DelayedData(String name, ByteDataView content, boolean isDirectory) {
+    private DelayedData(
+        String name, ByteDataView content, boolean isDirectory, boolean storeCompressed) {
       this.name = name;
       this.content = content;
       this.isDirectory = isDirectory;
+      this.storeCompressed = storeCompressed;
     }
 
     @Override
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index de093c4..ae6e72c 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -280,9 +280,6 @@
     itemFactory = proguardConfiguration.getDexItemFactory();
     enableTreeShaking = proguardConfiguration.isShrinking();
     enableMinification = proguardConfiguration.isObfuscating();
-    // TODO(b/244238384): Enable.
-    enableStringFormatOptimization =
-        System.getProperty("com.android.tools.r8.optimizeStringFormat") != null;
 
     if (!proguardConfiguration.isOptimizing()) {
       // TODO(b/171457102): Avoid the need for this.
@@ -328,7 +325,6 @@
     disableGlobalOptimizations();
     enableNameReflectionOptimization = false;
     enableStringConcatenationOptimization = false;
-    enableStringFormatOptimization = false;
   }
 
   public void disableGlobalOptimizations() {
@@ -413,7 +409,6 @@
   public boolean enableNameReflectionOptimization = true;
   public boolean enableStringConcatenationOptimization = true;
   // Enabled only for R8 (not D8).
-  public boolean enableStringFormatOptimization;
   public boolean enableTreeShakingOfLibraryMethodOverrides = false;
   public boolean encodeChecksums = false;
   public BiPredicate<String, Long> dexClassChecksumFilter = (name, checksum) -> true;
diff --git a/src/main/java/com/android/tools/r8/utils/ZipUtils.java b/src/main/java/com/android/tools/r8/utils/ZipUtils.java
index e02d3e0..2b11a9f 100644
--- a/src/main/java/com/android/tools/r8/utils/ZipUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ZipUtils.java
@@ -37,6 +37,7 @@
 import java.util.Spliterator;
 import java.util.Spliterators;
 import java.util.function.BiFunction;
+import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
@@ -100,6 +101,15 @@
     }
   }
 
+  public static void iter(Path zipFilePath, Consumer<ZipEntry> entryConsumer) throws IOException {
+    try (ZipFile zipFile = new ZipFile(zipFilePath.toFile(), StandardCharsets.UTF_8)) {
+      final Enumeration<? extends ZipEntry> entries = zipFile.entries();
+      while (entries.hasMoreElements()) {
+        entryConsumer.accept(entries.nextElement());
+      }
+    }
+  }
+
   @SuppressWarnings("UnnecessaryParentheses")
   public static boolean containsEntry(Path zipfile, String name) throws IOException {
     BooleanBox result = new BooleanBox();
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java
index 8273c96..e0cca4f 100644
--- a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java
+++ b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java
@@ -4,7 +4,11 @@
 
 package com.android.build.shrinker.r8integration;
 
+import com.android.aapt.Resources.ConfigValue;
+import com.android.aapt.Resources.Entry;
+import com.android.aapt.Resources.Item;
 import com.android.aapt.Resources.ResourceTable;
+import com.android.aapt.Resources.Value;
 import com.android.build.shrinker.NoDebugReporter;
 import com.android.build.shrinker.ResourceShrinkerModel;
 import com.android.build.shrinker.ResourceTableUtilKt;
@@ -16,7 +20,9 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 public class R8ResourceShrinkerState {
 
@@ -38,12 +44,17 @@
   }
 
   public static class R8ResourceShrinkerModel extends ResourceShrinkerModel {
+    private final Map<Integer, String> stringResourcesWithSingleValue = new HashMap<>();
 
     public R8ResourceShrinkerModel(
         ShrinkerDebugReporter debugReporter, boolean supportMultipackages) {
       super(debugReporter, supportMultipackages);
     }
 
+    public Map<Integer, String> getStringResourcesWithSingleValue() {
+      return stringResourcesWithSingleValue;
+    }
+
     // Similar to instantiation in ProtoResourceTableGatherer, but using an inputstream.
     void instantiateFromResourceTable(InputStream inputStream) {
       try {
@@ -60,14 +71,34 @@
           .forEachRemaining(
               entryWrapper -> {
                 ResourceType resourceType = ResourceType.fromClassName(entryWrapper.getType());
+                Entry entry = entryWrapper.getEntry();
+                int entryId = entryWrapper.getId();
+                recordSingleValueResources(resourceType, entry, entryId);
                 if (resourceType != ResourceType.STYLEABLE) {
                   this.addResource(
                       resourceType,
                       entryWrapper.getPackageName(),
-                      ResourcesUtil.resourceNameToFieldName(entryWrapper.getEntry().getName()),
-                      entryWrapper.getId());
+                      ResourcesUtil.resourceNameToFieldName(entry.getName()),
+                      entryId);
                 }
               });
     }
+
+    private void recordSingleValueResources(ResourceType resourceType, Entry entry, int entryId) {
+      if (!entry.hasOverlayableItem() && entry.getConfigValueList().size() == 1) {
+        if (resourceType == ResourceType.STRING) {
+          ConfigValue configValue = entry.getConfigValue(0);
+          if (configValue.hasValue()) {
+            Value value = configValue.getValue();
+            if (value.hasItem()) {
+              Item item = value.getItem();
+              if (item.hasStr()) {
+                stringResourcesWithSingleValue.put(entryId, item.getStr().getValue());
+              }
+            }
+          }
+        }
+      }
+    }
   }
 }
diff --git a/src/test/java/com/android/tools/r8/R8TestBuilder.java b/src/test/java/com/android/tools/r8/R8TestBuilder.java
index 7ae3463..d1eb192 100644
--- a/src/test/java/com/android/tools/r8/R8TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/R8TestBuilder.java
@@ -291,6 +291,11 @@
     return self();
   }
 
+  public T enableOptimizedShrinking() {
+    builder.setResourceShrinkerConfiguration(b -> b.enableOptimizedShrinkingWithR8().build());
+    return self();
+  }
+
   /**
    * Allow info, warning, and error diagnostics.
    *
@@ -905,8 +910,8 @@
     getBuilder()
         .setAndroidResourceProvider(
             new ArchiveProtoAndroidResourceProvider(resources, new PathOrigin(resources)));
-    getBuilder().setAndroidResourceConsumer(new ArchiveProtoAndroidResourceConsumer(output));
-    self();
+    getBuilder()
+        .setAndroidResourceConsumer(new ArchiveProtoAndroidResourceConsumer(output, resources));
     return addProgramClassFileData(testResource.getRClass().getClassFileData());
   }
 
diff --git a/src/test/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java b/src/test/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java
index 0ee66c7..021902d 100644
--- a/src/test/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java
+++ b/src/test/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java
@@ -10,9 +10,12 @@
 import com.android.aapt.Resources.ConfigValue;
 import com.android.aapt.Resources.Item;
 import com.android.aapt.Resources.ResourceTable;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestBase.Backend;
 import com.android.tools.r8.TestRuntime.CfRuntime;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.transformers.ClassTransformer;
 import com.android.tools.r8.transformers.MethodTransformer;
 import com.android.tools.r8.utils.AndroidApiLevel;
@@ -34,12 +37,16 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.Set;
 import java.util.TreeMap;
+import java.util.TreeSet;
 import java.util.stream.Collectors;
 import org.junit.Assert;
 import org.junit.rules.TemporaryFolder;
 
 public class AndroidResourceTestingUtils {
+  private static final String RESOURCES_DESCRIPTOR =
+      TestBase.descriptor(com.android.tools.r8.androidresources.Resources.class);
 
   enum RClassType {
     STRING,
@@ -53,6 +60,32 @@
     }
   }
 
+  public static Path resourcesClassAsDex(TemporaryFolder temp) throws Exception {
+    return TestBase.testForD8(temp, Backend.DEX)
+        .addProgramClassFileData(resouresClassAsJavaClass())
+        .compile()
+        .writeToZip();
+  }
+
+  public static byte[] resouresClassAsJavaClass() throws IOException {
+    return transformer(com.android.tools.r8.androidresources.Resources.class)
+        .setClassDescriptor(DexItemFactory.androidResourcesDescriptorString)
+        .replaceClassDescriptorInMethodInstructions(
+            RESOURCES_DESCRIPTOR, DexItemFactory.androidResourcesDescriptorString)
+        .replaceClassDescriptorInMembers(
+            RESOURCES_DESCRIPTOR, DexItemFactory.androidResourcesDescriptorString)
+        .transform();
+  }
+
+  public static byte[] transformResourcesReferences(Class clazz) throws IOException {
+    return transformer(clazz)
+        .replaceClassDescriptorInMethodInstructions(
+            RESOURCES_DESCRIPTOR, DexItemFactory.androidResourcesDescriptorString)
+        .replaceClassDescriptorInMembers(
+            RESOURCES_DESCRIPTOR, DexItemFactory.androidResourcesDescriptorString)
+        .transform();
+  }
+
   private static String rClassWithoutNamespaceAndOuter(Class clazz) {
     return rClassWithoutNamespaceAndOuter(clazz.getName());
   }
@@ -223,6 +256,8 @@
   public static class AndroidTestResourceBuilder {
     private String manifest;
     private final Map<String, String> stringValues = new TreeMap<>();
+    private final Set<String> stringValuesWithExtraLanguage = new TreeSet<>();
+    private final Map<String, String> overlayableValues = new TreeMap<>();
     private final Map<String, Integer> styleables = new TreeMap<>();
     private final Map<String, byte[]> drawables = new TreeMap<>();
     private final Map<String, String> xmlFiles = new TreeMap<>();
@@ -244,7 +279,7 @@
             addStringValue(name, name);
           }
           if (rClassType == RClassType.DRAWABLE) {
-            addDrawable(name, TINY_PNG);
+            addDrawable(name + ".png", TINY_PNG);
           }
           if (rClassType == RClassType.STYLEABLE) {
             // Add 4 different values, i.e., the array will be 4 integers.
@@ -282,6 +317,16 @@
       return this;
     }
 
+    AndroidTestResourceBuilder addExtraLanguageString(String name) {
+      stringValuesWithExtraLanguage.add(name);
+      return this;
+    }
+
+    AndroidTestResourceBuilder setOverlayableFor(String type, String name) {
+      overlayableValues.put(type, name);
+      return this;
+    }
+
     AndroidTestResourceBuilder setPackageId(int packageId) {
       this.packageId = packageId;
       return this;
@@ -298,7 +343,16 @@
       Path resFolder = temp.newFolder("res").toPath();
       Path valuesFolder = temp.newFolder("res", "values").toPath();
       if (stringValues.size() > 0) {
-        FileUtils.writeTextFile(valuesFolder.resolve("strings.xml"), createStringResourceXml());
+        FileUtils.writeTextFile(
+            valuesFolder.resolve("strings.xml"), createStringResourceXml(false));
+        if (stringValuesWithExtraLanguage.size() > 0) {
+          Path languageValues = temp.newFolder("res", "values-da").toPath();
+          FileUtils.writeTextFile(
+              languageValues.resolve("strings.xml"), createStringResourceXml(true));
+        }
+      }
+      if (overlayableValues.size() > 0) {
+        FileUtils.writeTextFile(valuesFolder.resolve("overlayable.xml"), createOverlayableXml());
       }
       if (styleables.size() > 0) {
         FileUtils.writeTextFile(
@@ -401,11 +455,30 @@
           new AndroidTestRClass(rClassJavaFile, rewrittenRClassFiles), output);
     }
 
-    private String createStringResourceXml() {
+    private String createOverlayableXml() {
       StringBuilder stringBuilder = new StringBuilder("<resources>\n");
+
+      stringBuilder.append("<overlayable name=\"OurOverlayables\">\n");
+      stringBuilder.append("<policy type=\"public\">\n");
+      overlayableValues.forEach(
+          (type, name) ->
+              stringBuilder.append("<item type=\"" + type + "\" name=\"" + name + "\" />\n"));
+      stringBuilder.append("</policy>\n");
+      stringBuilder.append("</overlayable>");
+      stringBuilder.append("</resources>");
+      return stringBuilder.toString();
+    }
+
+    private String createStringResourceXml(boolean nonDefaultLanguage) {
+      StringBuilder stringBuilder = new StringBuilder("<resources>\n");
+      String languagePostFix = nonDefaultLanguage ? "_da" : "";
       stringValues.forEach(
-          (name, value) ->
-              stringBuilder.append("<string name=\"" + name + "\">" + value + "</string>\n"));
+          (name, value) -> {
+            if (!nonDefaultLanguage || stringValuesWithExtraLanguage.contains(name)) {
+              stringBuilder.append(
+                  "<string name=\"" + name + "\">" + value + languagePostFix + "</string>\n");
+            }
+          });
       stringBuilder.append("</resources>");
       return stringBuilder.toString();
     }
@@ -496,4 +569,5 @@
   // 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};
+
 }
diff --git a/src/test/java/com/android/tools/r8/androidresources/Resources.java b/src/test/java/com/android/tools/r8/androidresources/Resources.java
new file mode 100644
index 0000000..ef01a99
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/androidresources/Resources.java
@@ -0,0 +1,16 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.androidresources;
+
+public class Resources {
+  public static String GET_STRING_VALUE = "GET_STRING_VALUE";
+
+  // Returns the GET_STRING_VALUE  to be able to distinguish resource inlined values from values
+  // we get from this call (i.e., not inlined). Inlined values are the actual values from the
+  // resource table.
+  public String getString(int id) {
+    return GET_STRING_VALUE;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/androidresources/TestArchiveCompression.java b/src/test/java/com/android/tools/r8/androidresources/TestArchiveCompression.java
new file mode 100644
index 0000000..e7d0e79
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/androidresources/TestArchiveCompression.java
@@ -0,0 +1,108 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.androidresources;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResource;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResourceBuilder;
+import com.android.tools.r8.utils.ZipUtils;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.zip.ZipEntry;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class TestArchiveCompression extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection parameters() {
+    return getTestParameters().withDefaultDexRuntime().withAllApiLevels().build();
+  }
+
+  public static AndroidTestResource getTestResources(TemporaryFolder temp) throws Exception {
+    return new AndroidTestResourceBuilder()
+        .withSimpleManifestAndAppNameString()
+        .addRClassInitializeWithDefaultValues(R.string.class, R.drawable.class)
+        .build(temp);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    AndroidTestResource testResources = getTestResources(temp);
+    Path resourceZip = testResources.getResourceZip();
+    assertTrue(ZipUtils.containsEntry(resourceZip, "res/drawable/foobar.png"));
+    assertTrue(ZipUtils.containsEntry(resourceZip, "res/drawable/unused_drawable.png"));
+    assertTrue(ZipUtils.containsEntry(resourceZip, "AndroidManifest.xml"));
+    assertTrue(ZipUtils.containsEntry(resourceZip, "resources.pb"));
+
+    validateCompression(resourceZip);
+    Path resourceOutput = temp.newFile("resources_out.zip").toPath();
+    testForR8(parameters.getBackend())
+        .setMinApi(parameters)
+        .addProgramClasses(FooBar.class)
+        .addAndroidResources(testResources, resourceOutput)
+        .addKeepMainRule(FooBar.class)
+        .compile()
+        .run(parameters.getRuntime(), FooBar.class)
+        .assertSuccess();
+    assertTrue(ZipUtils.containsEntry(resourceOutput, "res/drawable/foobar.png"));
+    assertFalse(ZipUtils.containsEntry(resourceOutput, "res/drawable/unused_drawable.png"));
+    assertTrue(ZipUtils.containsEntry(resourceOutput, "AndroidManifest.xml"));
+    assertTrue(ZipUtils.containsEntry(resourceOutput, "resources.pb"));
+    validateCompression(resourceOutput);
+  }
+
+  private static void validateCompression(Path resourceZip) throws IOException {
+    ZipUtils.iter(
+        resourceZip,
+        entry -> {
+          if (entry.getName().endsWith(".png")) {
+            assertEquals(ZipEntry.STORED, entry.getMethod());
+          } else {
+            assertEquals(ZipEntry.DEFLATED, entry.getMethod());
+          }
+        });
+  }
+
+  public static class FooBar {
+
+    public static void main(String[] args) {
+      if (System.currentTimeMillis() == 0) {
+        System.out.println(R.drawable.foobar);
+        System.out.println(R.string.bar);
+        System.out.println(R.string.foo);
+      }
+    }
+  }
+
+  public static class R {
+
+    public static class string {
+
+      public static int bar;
+      public static int foo;
+      public static int unused_string;
+    }
+
+    public static class drawable {
+
+      public static int foobar;
+      public static int unused_drawable;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/androidresources/TestResourceInlining.java b/src/test/java/com/android/tools/r8/androidresources/TestResourceInlining.java
new file mode 100644
index 0000000..bd97b91
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/androidresources/TestResourceInlining.java
@@ -0,0 +1,173 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.androidresources;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.R8TestBuilder;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResource;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResourceBuilder;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.List;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class TestResourceInlining extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameter(1)
+  public boolean optimize;
+
+  @Parameter(2)
+  public boolean addResourcesSubclass;
+
+  @Parameters(name = "{0}, optimize: {1}, addResourcesSubclass: {2}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withDefaultDexRuntime().withAllApiLevels().build(),
+        BooleanUtils.values(),
+        BooleanUtils.values());
+  }
+
+  public static AndroidTestResource getTestResources(TemporaryFolder temp) throws Exception {
+    return new AndroidTestResourceBuilder()
+        .withSimpleManifestAndAppNameString()
+        .addRClassInitializeWithDefaultValues(R.string.class)
+        .setOverlayableFor("string", "overlayable")
+        .addExtraLanguageString("bar")
+        .build(temp);
+  }
+
+  public static Path getAndroidResourcesClassInJar(TemporaryFolder temp) throws Exception {
+    byte[] androidResourcesClass =
+        transformer(Resources.class)
+            .setClassDescriptor(DexItemFactory.androidResourcesDescriptorString)
+            .transform();
+    return testForR8(temp, Backend.DEX)
+        .addProgramClassFileData(androidResourcesClass)
+        .compile()
+        .writeToZip();
+  }
+
+  public byte[] getResourcesSubclass() throws IOException {
+    return transformer(ResourcesSubclass.class)
+        .setSuper(DexItemFactory.androidResourcesDescriptorString)
+        .replaceClassDescriptorInMethodInstructions(
+            descriptor(Resources.class), DexItemFactory.androidResourcesDescriptorString)
+        .transform();
+  }
+
+  @Test
+  public void testR8Optimized() throws Exception {
+    testForR8(parameters.getBackend())
+        .setMinApi(parameters)
+        .addProgramClassFileData(
+            AndroidResourceTestingUtils.transformResourcesReferences(FooBar.class))
+        .applyIf(
+            addResourcesSubclass,
+            builder -> {
+              builder.addProgramClassFileData(getResourcesSubclass());
+              builder.addKeepClassRules(ResourcesSubclass.class);
+            })
+        .addAndroidResources(getTestResources(temp))
+        .addKeepMainRule(FooBar.class)
+        .applyIf(optimize, R8TestBuilder::enableOptimizedShrinking)
+        .addRunClasspathFiles(AndroidResourceTestingUtils.resourcesClassAsDex(temp))
+        .compile()
+        .inspectShrunkenResources(
+            resourceTableInspector -> {
+              // We should eventually remove this when optimizing
+              resourceTableInspector.assertContainsResourceWithName("string", "foo");
+              // Has multiple values, don't inline
+              resourceTableInspector.assertContainsResourceWithName("string", "bar");
+              // Has overlayable value, don't inline
+              resourceTableInspector.assertContainsResourceWithName("string", "bar");
+              resourceTableInspector.assertDoesNotContainResourceWithName(
+                  "string", "unused_string");
+            })
+        .inspect(
+            inspector -> {
+              // We should have removed one of the calls to getString if we are optimizing.
+              MethodSubject mainMethodSubject = inspector.clazz(FooBar.class).mainMethod();
+              assertThat(mainMethodSubject, isPresent());
+              assertEquals(
+                  mainMethodSubject
+                      .streamInstructions()
+                      .filter(InstructionSubject::isInvokeVirtual)
+                      .filter(
+                          i ->
+                              i.getMethod()
+                                  .holder
+                                  .descriptor
+                                  .toString()
+                                  .equals(DexItemFactory.androidResourcesDescriptorString))
+                      .count(),
+                  optimize && !addResourcesSubclass ? 2 : 3);
+            })
+        .run(parameters.getRuntime(), FooBar.class)
+        .applyIf(
+            optimize && !addResourcesSubclass,
+            result -> {
+              result.assertSuccessWithOutputLines(
+                  "foo", Resources.GET_STRING_VALUE, Resources.GET_STRING_VALUE);
+            })
+        .applyIf(
+            !optimize || addResourcesSubclass,
+            result -> {
+              result.assertSuccessWithOutputLines(
+                  Resources.GET_STRING_VALUE,
+                  Resources.GET_STRING_VALUE,
+                  Resources.GET_STRING_VALUE);
+            });
+  }
+
+  public static class FooBar {
+
+    public static void main(String[] args) {
+      Resources resources = new Resources();
+      // Ensure that we correctly handle the out value propagation
+      String s = resources.getString(R.string.foo);
+      String t = "X";
+      String u = System.currentTimeMillis() > 0 ? s : t;
+      System.out.println(u);
+      System.out.println(resources.getString(R.string.bar));
+      System.out.println(resources.getString(R.string.overlayable));
+    }
+  }
+
+  public static class ResourcesSubclass extends Resources {
+
+    @Override
+    public String getString(int id) {
+      System.out.println("foobar");
+      return super.getString(id);
+    }
+  }
+
+  public static class R {
+
+    public static class string {
+      public static int foo;
+      public static int bar;
+      public static int overlayable;
+      public static int unused_string;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/annotations/RetentionPolicyTest.java b/src/test/java/com/android/tools/r8/annotations/RetentionPolicyTest.java
index da0f3d1..8900bd3 100644
--- a/src/test/java/com/android/tools/r8/annotations/RetentionPolicyTest.java
+++ b/src/test/java/com/android/tools/r8/annotations/RetentionPolicyTest.java
@@ -35,8 +35,7 @@
   private static final Collection<Class<?>> CLASSES =
       ImmutableList.of(ClassRetained.class, SourceRetained.class, RuntimeRetained.class, A.class);
 
-  private static final String EXPECTED =
-      StringUtils.lines("@" + RuntimeRetained.class.getName() + "()");
+  private static final String EXPECTED = StringUtils.lines(RuntimeRetained.class.getName());
 
   @Parameters(name = "{0}, intermediate:{1}")
   public static List<Object[]> data() {
@@ -66,7 +65,7 @@
 
     public static void main(String[] args) {
       for (Annotation annotation : A.class.getAnnotations()) {
-        System.out.println(annotation);
+        System.out.println(annotation.annotationType().getName());
       }
     }
   }
diff --git a/src/test/java/com/android/tools/r8/examples/regress_62300145/Regress.java b/src/test/java/com/android/tools/r8/examples/regress_62300145/Regress.java
index 037cc1d..a3ea4b1 100644
--- a/src/test/java/com/android/tools/r8/examples/regress_62300145/Regress.java
+++ b/src/test/java/com/android/tools/r8/examples/regress_62300145/Regress.java
@@ -38,7 +38,7 @@
         System.out.print(index++ + ": ");
       }
       for (Annotation annotation : annotations[i]) {
-        System.out.println(annotation);
+        System.out.println(annotation.annotationType().getName());
       }
     }
   }
diff --git a/src/test/java/com/android/tools/r8/examples/regress_62300145/Regress62300145TestRunner.java b/src/test/java/com/android/tools/r8/examples/regress_62300145/Regress62300145TestRunner.java
index a88c723..d4b0a0e 100644
--- a/src/test/java/com/android/tools/r8/examples/regress_62300145/Regress62300145TestRunner.java
+++ b/src/test/java/com/android/tools/r8/examples/regress_62300145/Regress62300145TestRunner.java
@@ -39,8 +39,8 @@
   @Override
   public String getExpected() {
     return StringUtils.joinLines(
-        "0: @com.android.tools.r8.examples.regress_62300145.Regress$A()",
-        "1: @com.android.tools.r8.examples.regress_62300145.Regress$A()",
+        "0: com.android.tools.r8.examples.regress_62300145.Regress$A",
+        "1: com.android.tools.r8.examples.regress_62300145.Regress$A",
         "2: ");
   }
 
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringFormatTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringFormatTest.java
index 0f49f15..fa21a9f 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringFormatTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringFormatTest.java
@@ -96,7 +96,6 @@
     R8TestRunResult result =
         testForR8(parameters.getBackend())
             .addProgramClasses(MAIN, TestFormattable.class)
-            .addOptionsModification(options -> options.enableStringFormatOptimization = true)
             .enableInliningAnnotations()
             .addKeepMainRule(MAIN)
             .setMinApi(parameters)
@@ -110,7 +109,6 @@
     R8TestRunResult result =
         testForR8(parameters.getBackend())
             .addProgramClasses(MAIN, TestFormattable.class)
-            .addOptionsModification(options -> options.enableStringFormatOptimization = true)
             .enableInliningAnnotations()
             .addKeepMainRule(MAIN)
             .setMinApi(parameters)
diff --git a/src/test/java/com/android/tools/r8/keepanno/doctests/UsesReflectionDocumentationTest.java b/src/test/java/com/android/tools/r8/keepanno/doctests/UsesReflectionDocumentationTest.java
index 0914396..1225db2 100644
--- a/src/test/java/com/android/tools/r8/keepanno/doctests/UsesReflectionDocumentationTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/doctests/UsesReflectionDocumentationTest.java
@@ -67,25 +67,21 @@
     }
   }
 
-  /// DOC START: UsesReflection on virtual method
-  /* DOC TEXT START:
-  <p>If for example, your program is reflectively invoking a virtual method on some base class, you
-  should annotate the method that is performing the reflection with an annotation describing what
-  assumptions the reflective code is making.
+  /* INCLUDE DOC: UsesReflectionOnVirtualMethod
+  For example, if your program is reflectively invoking a method, you
+  should annotate the method that is doing the reflection. The annotation must describe the
+  assumptions the reflective code makes.
 
-  <p>In the following example, the method `foo` is looking up the method with the name
+  In the following example, the method `foo` is looking up the method with the name
   `hiddenMethod` on objects that are instances of `BaseClass`. It is then invoking the method with
-   no other arguments than the receiver.
+  no other arguments than the receiver.
 
-  <p>The minimal requirement for this code to work is therefore that all methods with the name
-  `hiddenMethod` and the empty list of parameters are targeted if they are objects that are
-  instances of the class `BaseClass` or subclasses thereof.
+  The assumptions the code makes are that all methods with the name
+  `hiddenMethod` and the empty list of parameters must remain valid for `getDeclaredMethod` if they
+  are objects that are instances of the class `BaseClass` or subclasses thereof.
+  INCLUDE END */
 
-  <p>By placing the `UsesReflection` annotation on the method `foo` the annotation is only in
-  effect if the method `foo` is determined to be used by the shrinker.
-  So, if `foo` turns out to be dead code then the shrinker can remove `foo` and also ignore the
-  keep annotation.
-  DOC TEXT END */
+  // INCLUDE CODE: UsesReflectionOnVirtualMethod
   static class MyClass {
 
     @UsesReflection({
@@ -99,7 +95,7 @@
     }
   }
 
-  // DOC END
+  // INCLUDE END
 
   static class TestClass {
 
diff --git a/src/test/java/com/android/tools/r8/keepanno/utils/DocPrinterBase.java b/src/test/java/com/android/tools/r8/keepanno/utils/DocPrinterBase.java
index f193da6..e83aed8 100644
--- a/src/test/java/com/android/tools/r8/keepanno/utils/DocPrinterBase.java
+++ b/src/test/java/com/android/tools/r8/keepanno/utils/DocPrinterBase.java
@@ -13,10 +13,16 @@
 public abstract class DocPrinterBase<T> {
 
   private String title = null;
+  private String returnDesc = null;
+  private String deprecatedDesc = null;
   private final List<String> additionalLines = new ArrayList<>();
 
   public abstract T self();
 
+  private boolean isEmptyOrJustTitle() {
+    return returnDesc == null && deprecatedDesc == null && additionalLines.isEmpty();
+  }
+
   public T clearDocLines() {
     additionalLines.clear();
     return self();
@@ -29,6 +35,20 @@
     return self();
   }
 
+  public T setDocReturn(String desc) {
+    returnDesc = desc;
+    return self();
+  }
+
+  public boolean isDeprecated() {
+    return deprecatedDesc != null;
+  }
+
+  public T setDeprecated(String desc) {
+    deprecatedDesc = desc;
+    return self();
+  }
+
   public T addParagraph(String... lines) {
     return addParagraph(Arrays.asList(lines));
   }
@@ -67,11 +87,11 @@
   }
 
   public void printDoc(Consumer<String> println) {
-    assert additionalLines.isEmpty() || title != null;
     if (title == null) {
+      assert isEmptyOrJustTitle();
       return;
     }
-    if (additionalLines.isEmpty()) {
+    if (isEmptyOrJustTitle()) {
       println.accept("/** " + title + " */");
       return;
     }
@@ -83,6 +103,12 @@
       }
       println.accept(" * " + line);
     }
+    if (returnDesc != null) {
+      println.accept(" * @return " + returnDesc);
+    }
+    if (deprecatedDesc != null) {
+      println.accept(" * @deprecated " + deprecatedDesc);
+    }
     println.accept(" */");
   }
 }
diff --git a/src/test/java/com/android/tools/r8/keepanno/utils/KeepAnnoMarkdownGenerator.java b/src/test/java/com/android/tools/r8/keepanno/utils/KeepAnnoMarkdownGenerator.java
new file mode 100644
index 0000000..f4fd1a8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/utils/KeepAnnoMarkdownGenerator.java
@@ -0,0 +1,247 @@
+// 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.keepanno.utils;
+
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.keepanno.annotations.KeepBinding;
+import com.android.tools.r8.keepanno.annotations.KeepCondition;
+import com.android.tools.r8.keepanno.annotations.KeepEdge;
+import com.android.tools.r8.keepanno.annotations.KeepForApi;
+import com.android.tools.r8.keepanno.annotations.KeepTarget;
+import com.android.tools.r8.keepanno.annotations.UsedByNative;
+import com.android.tools.r8.keepanno.annotations.UsedByReflection;
+import com.android.tools.r8.keepanno.annotations.UsesReflection;
+import com.android.tools.r8.keepanno.doctests.UsesReflectionDocumentationTest;
+import com.android.tools.r8.keepanno.utils.KeepItemAnnotationGenerator.Generator;
+import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableMap;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class KeepAnnoMarkdownGenerator {
+
+  public static void generateMarkdownDoc(Generator generator) {
+    try {
+      new KeepAnnoMarkdownGenerator(generator).internalGenerateMarkdownDoc();
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  private static String JAVADOC_URL =
+      "https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/";
+
+  private static final String INCLUDE_MD_START = "[[[INCLUDE";
+  private static final String INCLUDE_MD_DOC_START = "[[[INCLUDE DOC";
+  private static final String INCLUDE_MD_CODE_START = "[[[INCLUDE CODE";
+  private static final String INCLUDE_MD_END = "]]]";
+
+  private static final String INCLUDE_DOC_START = "INCLUDE DOC:";
+  private static final String INCLUDE_DOC_END = "INCLUDE END";
+  private static final String INCLUDE_CODE_START = "INCLUDE CODE:";
+  private static final String INCLUDE_CODE_END = "INCLUDE END";
+
+  private final Generator generator;
+  private final Map<String, String> typeLinkReplacements;
+  private Map<String, String> docReplacements = new HashMap<>();
+  private Map<String, String> codeReplacements = new HashMap<>();
+
+  public KeepAnnoMarkdownGenerator(Generator generator) {
+    this.generator = generator;
+    typeLinkReplacements =
+        getTypeLinkReplacements(
+            KeepEdge.class,
+            KeepBinding.class,
+            KeepTarget.class,
+            KeepCondition.class,
+            UsesReflection.class,
+            UsedByReflection.class,
+            UsedByNative.class,
+            KeepForApi.class);
+    populateCodeAndDocReplacements(UsesReflectionDocumentationTest.class);
+  }
+
+  private Map<String, String> getTypeLinkReplacements(Class<?>... classes) {
+    ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
+    for (Class<?> clazz : classes) {
+      builder.put("`@" + clazz.getSimpleName() + "`", getMdLink(clazz));
+    }
+    return builder.build();
+  }
+
+  private void populateCodeAndDocReplacements(Class<?>... classes) {
+    try {
+      for (Class<?> clazz : classes) {
+        Path sourceFile = ToolHelper.getSourceFileForTestClass(clazz);
+        String text = FileUtils.readTextFile(sourceFile, StandardCharsets.UTF_8);
+        extractMarkers(text, INCLUDE_DOC_START, INCLUDE_DOC_END, docReplacements);
+        extractMarkers(text, INCLUDE_CODE_START, INCLUDE_CODE_END, codeReplacements);
+      }
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  private static void extractMarkers(
+      String text, String markerKeyStart, String markerKeyEnd, Map<String, String> replacementMap) {
+    int index = text.indexOf(markerKeyStart);
+    while (index >= 0) {
+      int markerTitleEnd = text.indexOf('\n', index);
+      if (markerTitleEnd < 0) {
+        throw new RuntimeException("Failed to find end marker title");
+      }
+      int end = text.indexOf(markerKeyEnd, index);
+      if (end < 0) {
+        throw new RuntimeException("Failed to find end marker");
+      }
+      int endBeforeNewLine = text.lastIndexOf('\n', end);
+      if (endBeforeNewLine < markerTitleEnd) {
+        throw new RuntimeException("No new-line before end marker");
+      }
+      String markerTitle = text.substring(index + markerKeyStart.length(), markerTitleEnd);
+      String includeContent = text.substring(markerTitleEnd + 1, endBeforeNewLine);
+      String old = replacementMap.put(markerTitle.trim(), includeContent);
+      if (old != null) {
+        throw new RuntimeException("Duplicate definition of marker");
+      }
+      index = text.indexOf(markerKeyStart, end);
+    }
+  }
+
+  private String getMdLink(Class<?> clazz) {
+    String url = JAVADOC_URL + clazz.getTypeName().replace('.', '/') + ".html";
+    return "[@" + clazz.getSimpleName() + "](" + url + ")";
+  }
+
+  private void println() {
+    generator.println("");
+  }
+
+  private void println(String line) {
+    generator.println(line);
+  }
+
+  private void internalGenerateMarkdownDoc() throws IOException {
+    Path template = Paths.get("doc/keepanno-guide.template.md");
+    println("[comment]: <> (DO NOT EDIT - GENERATED FILE)");
+    println("[comment]: <> (Changes should be made in " + template + ")");
+    println();
+    List<String> readAllLines = FileUtils.readAllLines(template);
+    for (int i = 0; i < readAllLines.size(); i++) {
+      String line = readAllLines.get(i);
+      try {
+        processLine(line, generator);
+      } catch (Exception e) {
+        System.err.println("Parse error on line " + (i + 1) + ":");
+        System.err.println(line);
+        System.err.println(e.getMessage());
+      }
+    }
+  }
+
+  private String replaceCodeAndDocMarkers(String line) {
+    String originalLine = line;
+    line = line.trim();
+    if (!line.startsWith(INCLUDE_MD_START)) {
+      return originalLine;
+    }
+    int keyStartIndex = line.indexOf(':');
+    if (!line.endsWith(INCLUDE_MD_END) || keyStartIndex < 0) {
+      throw new RuntimeException("Invalid include directive");
+    }
+    String key = line.substring(keyStartIndex + 1, line.length() - INCLUDE_MD_END.length());
+    if (line.startsWith(INCLUDE_MD_DOC_START)) {
+      return replaceDoc(key);
+    }
+    if (line.startsWith(INCLUDE_MD_CODE_START)) {
+      return replaceCode(key);
+    }
+    throw new RuntimeException("Unknown replacement marker");
+  }
+
+  private String replaceDoc(String key) {
+    String replacement = docReplacements.get(key);
+    if (replacement == null) {
+      throw new RuntimeException("No replacement defined for " + key);
+    }
+    return unindentLines(replacement, new StringBuilder()).toString();
+  }
+
+  private String replaceCode(String key) {
+    String replacement = codeReplacements.get(key);
+    if (replacement == null) {
+      throw new RuntimeException("No replacement defined for " + key);
+    }
+    StringBuilder builder = new StringBuilder();
+    builder.append("```\n");
+    unindentLines(replacement, builder);
+    builder.append("```\n");
+    return builder.toString();
+  }
+
+  private StringBuilder unindentLines(String replacement, StringBuilder builder) {
+    int shortestSpacePrefix = Integer.MAX_VALUE;
+    List<String> lines = StringUtils.split(replacement, '\n');
+    for (String line : lines) {
+      if (!line.isEmpty()) {
+        shortestSpacePrefix = Math.min(shortestSpacePrefix, findFirstNonSpaceIndex(line));
+      }
+    }
+    if (shortestSpacePrefix > 0) {
+      for (String line : lines) {
+        if (!line.isEmpty()) {
+          builder.append(line.substring(shortestSpacePrefix));
+        }
+        builder.append('\n');
+      }
+    } else {
+      builder.append(replacement);
+      builder.append('\n');
+    }
+    return builder;
+  }
+
+  private int findFirstNonSpaceIndex(String line) {
+    for (int i = 0; i < line.length(); i++) {
+      if (line.charAt(i) != ' ') {
+        return i;
+      }
+    }
+    return line.length();
+  }
+
+  private String tryLinkReplacements(String line) {
+    int index = line.indexOf("`@");
+    if (index < 0) {
+      return null;
+    }
+    int end = line.indexOf('`', index + 1);
+    if (end < 0) {
+      throw new RuntimeException("No end marker on line: " + line);
+    }
+    String typeLink = line.substring(index, end + 1);
+    String replacement = typeLinkReplacements.get(typeLink);
+    if (replacement == null) {
+      throw new RuntimeException("Unknown type link: " + typeLink);
+    }
+    return line.replace(typeLink, replacement);
+  }
+
+  private void processLine(String line, Generator generator) {
+    line = replaceCodeAndDocMarkers(line);
+    String replacement = tryLinkReplacements(line);
+    while (replacement != null) {
+      line = replacement;
+      replacement = tryLinkReplacements(line);
+    }
+    generator.println(line);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/utils/KeepItemAnnotationGenerator.java b/src/test/java/com/android/tools/r8/keepanno/utils/KeepItemAnnotationGenerator.java
index 67a5fe0..005c344 100644
--- a/src/test/java/com/android/tools/r8/keepanno/utils/KeepItemAnnotationGenerator.java
+++ b/src/test/java/com/android/tools/r8/keepanno/utils/KeepItemAnnotationGenerator.java
@@ -72,6 +72,9 @@
 
     void generate(Generator generator) {
       printDoc(generator::println);
+      if (isDeprecated()) {
+        generator.println("@Deprecated");
+      }
       if (valueDefault == null) {
         generator.println(valueType + " " + name + "();");
       } else {
@@ -203,7 +206,7 @@
     }
   }
 
-  private static class Generator {
+  public static class Generator {
 
     private static final List<Class<?>> ANNOTATION_IMPORTS =
         ImmutableList.of(ElementType.class, Retention.class, RetentionPolicy.class, Target.class);
@@ -222,13 +225,14 @@
     }
 
     private void println() {
-      // Don't indent empty lines.
-      writer.println();
+      println("");
     }
 
-    private void println(String line) {
-      assert line.length() > 0;
-      writer.print(Strings.repeat(" ", indent));
+    public void println(String line) {
+      // Don't indent empty lines.
+      if (line.length() > 0) {
+        writer.print(Strings.repeat(" ", indent));
+      }
       writer.println(line);
     }
 
@@ -265,6 +269,7 @@
           .addMember(
               new GroupMember("description")
                   .setDocTitle("Optional description to document the reason for this annotation.")
+                  .setDocReturn("The descriptive message. Defaults to no description.")
                   .defaultEmptyString());
     }
 
@@ -279,8 +284,9 @@
               new GroupMember("preconditions")
                   .setDocTitle(
                       "Conditions that should be satisfied for the annotation to be in effect.")
-                  .addParagraph(
-                      "Defaults to no conditions, thus trivially/unconditionally satisfied.")
+                  .setDocReturn(
+                      "The list of preconditions. "
+                          + "Defaults to no conditions, thus trivially/unconditionally satisfied.")
                   .defaultEmptyArray(KeepCondition.class));
     }
 
@@ -289,6 +295,7 @@
           .addMember(
               new GroupMember("consequences")
                   .setDocTitle("Consequences that must be kept if the annotation is in effect.")
+                  .setDocReturn("The list of target consequences.")
                   .requiredValueOfArrayType(KeepTarget.class));
     }
 
@@ -297,6 +304,7 @@
           .addMember(
               new GroupMember("value")
                   .setDocTitle("Consequences that must be kept if the annotation is in effect.")
+                  .setDocReturn("The list of target consequences.")
                   .requiredValueOfArrayType(KeepTarget.class));
     }
 
@@ -305,7 +313,9 @@
           .addMember(
               new GroupMember("additionalPreconditions")
                   .setDocTitle("Additional preconditions for the annotation to be in effect.")
-                  .addParagraph("Defaults to no additional preconditions.")
+                  .setDocReturn(
+                      "The list of additional preconditions. "
+                          + "Defaults to no additional preconditions.")
                   .defaultEmptyArray("KeepCondition"));
     }
 
@@ -314,7 +324,9 @@
           .addMember(
               new GroupMember("additionalTargets")
                   .setDocTitle(docTitle)
-                  .addParagraph("Defaults to no additional targets.")
+                  .setDocReturn(
+                      "List of additional target consequences. "
+                          + "Defaults to no additional target consequences.")
                   .defaultEmptyArray("KeepTarget"));
     }
 
@@ -327,6 +339,7 @@
           .defaultType("KeepItemKind")
           .defaultValue("KeepItemKind.DEFAULT")
           .setDocTitle("Specify the kind of this item pattern.")
+          .setDocReturn("The kind for this pattern.")
           .addParagraph("Possible values are:")
           .addUnorderedList(
               KeepItemKind.ONLY_CLASS.name(),
@@ -343,15 +356,16 @@
       return new Group(OPTIONS_GROUP)
           .addMember(
               new GroupMember("allow")
-                  .setDocTitle(
-                      "Define the "
-                          + OPTIONS_GROUP
-                          + " that do not need to be preserved for the target.")
+                  .setDocTitle("Define the " + OPTIONS_GROUP + " that are allowed to be modified.")
+                  .addParagraph("The specified options do not need to be preserved for the target.")
+                  .setDocReturn("Options allowed to be modified for the target.")
                   .defaultEmptyArray("KeepOption"))
           .addMember(
               new GroupMember("disallow")
                   .setDocTitle(
-                      "Define the " + OPTIONS_GROUP + " that *must* be preserved for the target.")
+                      "Define the " + OPTIONS_GROUP + " that are not allowed to be modified.")
+                  .addParagraph("The specified options *must* be preserved for the target.")
+                  .setDocReturn("Options not allowed to be modified for the target.")
                   .defaultEmptyArray("KeepOption"))
           .addDocFooterParagraph(
               "If nothing is specified for "
@@ -368,12 +382,14 @@
           .setDocTitle(
               "Name with which other bindings, conditions or targets "
                   + "can reference the bound item pattern.")
+          .setDocReturn("Name of the binding.")
           .requiredStringValue();
     }
 
     private GroupMember classFromBinding() {
       return new GroupMember("classFromBinding")
           .setDocTitle("Define the " + CLASS_GROUP + " pattern by reference to a binding.")
+          .setDocReturn("The name of the binding that defines the class.")
           .defaultEmptyString();
     }
 
@@ -386,6 +402,7 @@
     private GroupMember className() {
       return new GroupMember("className")
           .setDocTitle("Define the " + CLASS_NAME_GROUP + " pattern by fully qualified class name.")
+          .setDocReturn("The qualified class name that defines the class.")
           .defaultEmptyString();
     }
 
@@ -393,6 +410,7 @@
       return new GroupMember("classConstant")
           .setDocTitle(
               "Define the " + CLASS_NAME_GROUP + " pattern by reference to a Class constant.")
+          .setDocReturn("The class-constant that defines the class.")
           .defaultObjectClass();
     }
 
@@ -409,6 +427,7 @@
               "Define the "
                   + INSTANCE_OF_GROUP
                   + " pattern as classes that are instances of the fully qualified class name.")
+          .setDocReturn("The qualified class name that defines what instance-of the class must be.")
           .defaultEmptyString();
     }
 
@@ -418,6 +437,7 @@
               "Define the "
                   + INSTANCE_OF_GROUP
                   + " pattern as classes that are instances the referenced Class constant.")
+          .setDocReturn("The class constant that defines what instance-of the class must be.")
           .defaultObjectClass();
     }
 
@@ -433,6 +453,7 @@
               "Define the "
                   + INSTANCE_OF_GROUP
                   + " pattern as classes that are instances of the fully qualified class name.")
+          .setDocReturn("The qualified class name that defines what instance-of the class must be.")
           .addParagraph(getInstanceOfExclusiveDoc())
           .defaultEmptyString();
     }
@@ -444,6 +465,7 @@
                   + INSTANCE_OF_GROUP
                   + " pattern as classes that are instances the referenced Class constant.")
           .addParagraph(getInstanceOfExclusiveDoc())
+          .setDocReturn("The class constant that defines what instance-of the class must be.")
           .defaultObjectClass();
     }
 
@@ -454,7 +476,9 @@
                   + INSTANCE_OF_GROUP
                   + " pattern as classes extending the fully qualified class name.")
           .addParagraph(getInstanceOfExclusiveDoc())
-          .addParagraph("This property is deprecated, use instanceOfClassName instead.")
+          .setDeprecated(
+              "This property is deprecated, use " + docLink(instanceOfClassName()) + " instead.")
+          .setDocReturn("The class name that defines what the class must extend.")
           .defaultEmptyString();
     }
 
@@ -465,7 +489,11 @@
                   + INSTANCE_OF_GROUP
                   + " pattern as classes extending the referenced Class constant.")
           .addParagraph(getInstanceOfExclusiveDoc())
-          .addParagraph("This property is deprecated, use instanceOfClassConstant instead.")
+          .setDeprecated(
+              "This property is deprecated, use "
+                  + docLink(instanceOfClassConstant())
+                  + " instead.")
+          .setDocReturn("The class constant that defines what the class must extend.")
           .defaultObjectClass();
     }
 
@@ -490,6 +518,7 @@
                       "Mutually exclusive with all other class and member pattern properties.",
                       "When a member binding is referenced this item is defined to be that item,",
                       "including its class and member patterns.")
+                  .setDocReturn("The binding name that defines the member.")
                   .defaultEmptyString());
     }
 
@@ -501,6 +530,7 @@
                   .addParagraph(
                       "Mutually exclusive with all field and method properties",
                       "as use restricts the match to both types of members.")
+                  .setDocReturn("The member access-flag constraints that must be met.")
                   .defaultEmptyArray("MemberAccessFlags"));
     }
 
@@ -531,6 +561,7 @@
                   .setDocTitle("Define the method-access pattern by matching on access flags.")
                   .addParagraph(getMutuallyExclusiveForMethodProperties())
                   .addParagraph(getMethodDefaultDoc("any method-access flags"))
+                  .setDocReturn("The method access-flag constraints that must be met.")
                   .defaultEmptyArray("MethodAccessFlags"));
     }
 
@@ -541,6 +572,7 @@
                   .setDocTitle("Define the method-name pattern by an exact method name.")
                   .addParagraph(getMutuallyExclusiveForMethodProperties())
                   .addParagraph(getMethodDefaultDoc("any method name"))
+                  .setDocReturn("The exact method name of the method.")
                   .defaultEmptyString());
     }
 
@@ -552,6 +584,7 @@
                       "Define the method return-type pattern by a fully qualified type or 'void'.")
                   .addParagraph(getMutuallyExclusiveForMethodProperties())
                   .addParagraph(getMethodDefaultDoc("any return type"))
+                  .setDocReturn("The qualified type name of the method return type.")
                   .defaultEmptyString());
     }
 
@@ -563,6 +596,7 @@
                       "Define the method parameters pattern by a list of fully qualified types.")
                   .addParagraph(getMutuallyExclusiveForMethodProperties())
                   .addParagraph(getMethodDefaultDoc("any parameters"))
+                  .setDocReturn("The list of qualified type names of the method parameters.")
                   .defaultType("String[]")
                   .defaultValue("{\"<default>\"}"));
     }
@@ -574,6 +608,7 @@
                   .setDocTitle("Define the field-access pattern by matching on access flags.")
                   .addParagraph(getMutuallyExclusiveForFieldProperties())
                   .addParagraph(getFieldDefaultDoc("any field-access flags"))
+                  .setDocReturn("The field access-flag constraints that must be met.")
                   .defaultEmptyArray("FieldAccessFlags"));
     }
 
@@ -584,6 +619,7 @@
                   .setDocTitle("Define the field-name pattern by an exact field name.")
                   .addParagraph(getMutuallyExclusiveForFieldProperties())
                   .addParagraph(getFieldDefaultDoc("any field name"))
+                  .setDocReturn("The exact field name of the field.")
                   .defaultEmptyString());
     }
 
@@ -594,6 +630,7 @@
                   .setDocTitle("Define the field-type pattern by a fully qualified type.")
                   .addParagraph(getMutuallyExclusiveForFieldProperties())
                   .addParagraph(getFieldDefaultDoc("any type"))
+                  .setDocReturn("The qualified type name of the field type.")
                   .defaultEmptyString());
     }
 
@@ -674,12 +711,7 @@
       println();
       withIndent(
           () -> {
-            new GroupMember("bindingName")
-                .setDocTitle(
-                    "Name with which other bindings, conditions or targets can reference the bound"
-                        + " item pattern.")
-                .requiredValueOfType("String")
-                .generate(this);
+            bindingName().generate(this);
             println();
             getKindGroup().generate(this);
             println();
@@ -939,6 +971,10 @@
       return "{@link " + simpleName(clazz) + "}";
     }
 
+    private String docLink(GroupMember member) {
+      return "{@link #" + member.name + "}";
+    }
+
     private String docLink(KeepItemKind kind) {
       return "{@link KeepItemKind#" + kind.name() + "}";
     }
@@ -1236,11 +1272,16 @@
       PrintStream printStream = new PrintStream(byteStream);
       Generator generator = new Generator(printStream);
       fn.accept(generator);
-      String formatted = CodeGenerationBase.formatRawOutput(byteStream.toString());
+      String formatted = byteStream.toString();
+      if (file.toString().endsWith(".java")) {
+        formatted = CodeGenerationBase.formatRawOutput(formatted);
+      }
       Files.write(Paths.get(ToolHelper.getProjectRoot()).resolve(file), formatted.getBytes());
     }
 
     public static void run() throws IOException {
+      writeFile(Paths.get("doc/keepanno-guide.md"), KeepAnnoMarkdownGenerator::generateMarkdownDoc);
+
       Path keepAnnoRoot = Paths.get("src/keepanno/java/com/android/tools/r8/keepanno");
 
       Path astPkg = keepAnnoRoot.resolve("ast");
@@ -1260,4 +1301,5 @@
           g -> g.generateUsedByX("UsedByNative", "accessed from native code via JNI"));
     }
   }
+
 }
diff --git a/tools/archive.py b/tools/archive.py
index fd250dd..a906a82 100755
--- a/tools/archive.py
+++ b/tools/archive.py
@@ -122,6 +122,21 @@
     print('INFO: Open files hard limit: %s' % hard)
 
 
+def UploadDir(src_dir, version_or_path, dst_dir, is_main, options):
+    destination = GetUploadDestination(version_or_path, dst_dir, is_main)
+    print(f'Uploading {src_dir} to {destination}')
+    if options.dry_run:
+        if options.dry_run_output:
+            dry_run_destination = os.path.join(options.dry_run_output, version_or_path, dst_dir)
+            print(f'Dry run, not actually uploading. Copying to {dry_run_destination}')
+            shutil.copytree(src_dir, dry_run_destination)
+        else:
+            print('Dry run, not actually uploading')
+    else:
+        utils.upload_directory_to_cloud_storage(src_dir, destination)
+        print(f'Directory available at: {GetUrl(version_or_path, dst_dir, is_main)}')
+
+
 def Main():
     (options, args) = ParseOptions()
     Run(options)
@@ -171,7 +186,9 @@
         if (not options.skip_gradle_build):
             gradle.RunGradle([
                 utils.GRADLE_TASK_CONSOLIDATED_LICENSE,
-                utils.GRADLE_TASK_KEEP_ANNO_JAR, utils.GRADLE_TASK_R8,
+                utils.GRADLE_TASK_KEEP_ANNO_JAR,
+                utils.GRADLE_TASK_KEEP_ANNO_DOC,
+                utils.GRADLE_TASK_R8,
                 utils.GRADLE_TASK_R8LIB, utils.GRADLE_TASK_R8LIB_NO_DEPS,
                 utils.GRADLE_TASK_THREADING_MODULE_BLOCKING,
                 utils.GRADLE_TASK_THREADING_MODULE_SINGLE_THREADED,
@@ -229,6 +246,20 @@
             '-PspdxRevision=' + GetGitHash()
         ])
 
+        # Upload keep-anno javadoc to a fixed "docs" location.
+        if is_main:
+            version_or_path = 'docs'
+            dst_dir = 'keepanno/javadoc'
+            UploadDir(utils.KEEPANNO_ANNOTATIONS_DOC, version_or_path, dst_dir, is_main, options)
+
+        # Upload directories.
+        dirs_for_archiving = [
+            (utils.KEEPANNO_ANNOTATIONS_DOC, 'keepanno/javadoc'),
+        ]
+        for (src_dir, dst_dir) in dirs_for_archiving:
+            UploadDir(src_dir, version, dst_dir, is_main, options)
+
+        # Upload files.
         for_archiving = [
             utils.R8_JAR, utils.R8LIB_JAR, utils.R8LIB_JAR + '.map',
             utils.R8LIB_JAR + '_map.zip', utils.R8_FULL_EXCLUDE_DEPS_JAR,
@@ -258,8 +289,8 @@
             print('Uploading %s to %s' % (tagged_jar, destination))
             if options.dry_run:
                 if options.dry_run_output:
-                    dry_run_destination = os.path.join(options.dry_run_output,
-                                                       file_name)
+                    dry_run_destination = os.path.join(
+                        options.dry_run_output, version, file_name)
                     print('Dry run, not actually uploading. Copying to ' +
                           dry_run_destination)
                     shutil.copyfile(tagged_jar, dry_run_destination)
diff --git a/tools/compiledump.py b/tools/compiledump.py
index 2d1a3a8..ce09db8 100755
--- a/tools/compiledump.py
+++ b/tools/compiledump.py
@@ -11,6 +11,7 @@
 import zipfile
 
 import archive
+import gradle
 import jdk
 import retrace
 import utils
@@ -139,6 +140,18 @@
                         help='Run compilation in specified mode',
                         choices=['debug', 'release'],
                         default=None)
+    parser.add_argument(
+        '--ignore-features',
+        help="Don't split into features when features are present."
+            ' Instead include feature code in main app output.'
+            ' This is always the case when compiler is d8.',
+        default=False,
+        action='store_true')
+    parser.add_argument(
+        '--no-build',
+        help="Don't build when using --version main",
+        default=False,
+        action='store_true')
     return parser
 
 
@@ -348,7 +361,9 @@
 
 
 def determine_feature_output(feature_jar, temp):
-    return os.path.join(temp, os.path.basename(feature_jar)[:-4] + ".out.jar")
+    return os.path.join(
+        args.output if args.output and os.path.isdir(args.output) else temp,
+        os.path.basename(feature_jar)[:-4] + ".out.jar")
 
 
 def determine_program_jar(args, dump):
@@ -401,6 +416,10 @@
 def download_distribution(version, args, temp):
     nolib = args.nolib
     if version == 'main':
+        if not args.no_build:
+          gradle.RunGradle(
+            [utils.GRADLE_TASK_R8] if nolib else [utils.GRADLE_TASK_R8LIB]
+          )
         return utils.R8_JAR if nolib else utils.R8LIB_JAR
     if version == 'source':
         return '%s:%s' % (utils.BUILD_JAVA_MAIN_DIR, utils.ALL_DEPS_JAR)
@@ -574,10 +593,13 @@
         else:
             cmd.extend(['--source', program_jar])
         for feature_jar in dump.feature_jars():
-            cmd.extend([
-                '--feature-jar', feature_jar,
-                determine_feature_output(feature_jar, temp)
-            ])
+            if not args.ignore_features and compiler != 'd8':
+                cmd.extend([
+                    '--feature-jar', feature_jar,
+                    determine_feature_output(feature_jar, temp)
+                ])
+            else:
+                cmd.append(feature_jar)
         if dump.library_jar():
             cmd.extend(['--lib', dump.library_jar()])
         if dump.classpath_jar() and not is_l8_compiler(compiler):
diff --git a/tools/r8_release.py b/tools/r8_release.py
index 116a52c..ce83d32 100755
--- a/tools/r8_release.py
+++ b/tools/r8_release.py
@@ -121,9 +121,7 @@
                 validate_version_change_diff(version_diff_output, "main",
                                              version)
 
-                cmd = ['git', 'cl', 'upload', '--no-squash']
-                if args.bypass_hooks:
-                    cmd.append('--bypass-hooks')
+                cmd = ['git', 'cl', 'upload', '--no-squash', '--bypass-hooks']
                 maybe_check_call(args, cmd)
 
                 if args.dry_run:
diff --git a/tools/utils.py b/tools/utils.py
index 597f8b1..e859748 100644
--- a/tools/utils.py
+++ b/tools/utils.py
@@ -43,6 +43,7 @@
 GRADLE_TASK_CLEAN_TEST = ':test:cleanTest'
 GRADLE_TASK_CONSOLIDATED_LICENSE = ':main:consolidatedLicense'
 GRADLE_TASK_KEEP_ANNO_JAR = ':keepanno:keepAnnoAnnotationsJar'
+GRADLE_TASK_KEEP_ANNO_DOC = ':keepanno:keepAnnoAnnotationsDoc'
 GRADLE_TASK_R8 = ':main:r8WithRelocatedDeps'
 GRADLE_TASK_R8LIB = ':test:assembleR8LibWithRelocatedDeps'
 GRADLE_TASK_R8LIB_NO_DEPS = ':test:assembleR8LibNoDeps'
@@ -77,6 +78,7 @@
 LIBRARY_DESUGAR_CONVERSIONS_ZIP = os.path.join(
     CUSTOM_CONVERSION_DIR, 'library_desugar_conversions.jar')
 KEEPANNO_ANNOTATIONS_JAR = os.path.join(LIBS, 'keepanno-annotations.jar')
+KEEPANNO_ANNOTATIONS_DOC = os.path.join('d8_r8', 'keepanno', 'build', 'docs', 'javadoc')
 
 DESUGAR_CONFIGURATION = os.path.join('src', 'library_desugar',
                                      'desugar_jdk_libs.json')
@@ -391,6 +393,14 @@
     PrintCmd(cmd)
     subprocess.check_call(cmd)
 
+def upload_directory_to_cloud_storage(source, destination, parallel=True):
+    cmd = [get_gsutil()]
+    if parallel:
+        cmd += ['-m']
+    cmd += ['cp', '-R']
+    cmd += [source, destination]
+    PrintCmd(cmd)
+    subprocess.check_call(cmd)
 
 def delete_file_from_cloud_storage(destination):
     cmd = [get_gsutil(), 'rm', destination]