Merge commit 'ca83a4d86859229a3c1e5fe938125342931d5e77' into dev-release

Change-Id: Idf053ded2dd8d9b3b363a251b4439a1cd1ff1135
diff --git a/doc/keepanno-guide.md b/doc/keepanno-guide.md
index 4f7665a..c41aa8b 100644
--- a/doc/keepanno-guide.md
+++ b/doc/keepanno-guide.md
@@ -31,7 +31,7 @@
 
 
 
-## Introduction<a id="introduction"></a>
+## Introduction<a name="introduction"></a>
 
 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
@@ -50,7 +50,7 @@
 independent from keep rules and have a hopefully more clear and direct meaning.
 
 
-## Build configuration<a id="build-configuration"></a>
+## Build configuration<a name="build-configuration"></a>
 
 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,
@@ -77,7 +77,7 @@
 ```
 
 
-## Annotating code using reflection<a id="using-reflection"></a>
+## Annotating code using reflection<a name="using-reflection"></a>
 
 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.
@@ -85,7 +85,7 @@
 of such use cases are detailed below.
 
 
-### Invoking methods<a id="using-reflection-methods"></a>
+### Invoking methods<a name="using-reflection-methods"></a>
 
 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
@@ -117,7 +117,7 @@
 
 
 
-### Accessing fields<a id="using-reflection-fields"></a>
+### Accessing fields<a name="using-reflection-fields"></a>
 
 For example, if your program is reflectively accessing the fields on a class, you should
 annotate the method that is doing the reflection.
@@ -152,17 +152,153 @@
 
 
 
-## Annotating code used by reflection (or via JNI)<a id="used-by-reflection"></a>
+## Annotating code used by reflection (or via JNI)<a name="used-by-reflection"></a>
 
-TODO
+Sometimes reflecting code cannot be annotated. For example, the reflection can
+be done in native code or in a library outside your control. In such cases you
+can annotate the code that is being used by reflection with either
+[@UsedByReflection](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/UsedByReflection.html) or [@UsedByNative](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/UsedByNative.html). These two annotations are equivalent.
+Use the one that best matches why the annotation is needed.
+
+Let's consider some code with reflection outside our control.
+For example, the same field printing as in the above example might be part of a library.
+
+In this example, the `MyClassWithFields` is a class you are passing to the
+field-printing utility of the library. Since the library is reflectively accessing each field
+we annotate them with the [@UsedByReflection](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/UsedByReflection.html) annotation.
+
+We could additionally add the [@UsedByReflection.constraints](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/UsedByReflection.html#constraints()) property as we did previously.
+We elide it here for brevity.
 
 
-## Annotating APIs<a id="apis"></a>
+```
+public class MyClassWithFields implements PrintableFieldInterface {
+  @UsedByReflection final int intField = 42;
 
-TODO
+  @UsedByReflection String stringField = "Hello!";
+}
+
+public static void run() throws Exception {
+  new FieldValuePrinterLibrary().printFieldValues(new MyClassWithFields());
+}
+```
 
 
-## Migrating rules to annotations<a id="migrating-rules"></a>
+Rather than annotate the individual fields we can annotate the holder and add a specification
+similar to the [@KeepTarget](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepTarget.html). The [@UsedByReflection.kind](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/UsedByReflection.html#kind()) specifies that only the fields are
+used reflectively. In particular, the "field printer" example we are considering here does not
+make reflective assumptions about the holder class, so we should not constrain it.
+
+To be more precise let's add the [@UsedByReflection.constraints](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/UsedByReflection.html#constraints()) property now. This specifies
+that the fields are looked up, their names are used/assumed and their values are read.
+
+
+```
+@UsedByReflection(
+    kind = KeepItemKind.ONLY_FIELDS,
+    constraints = {KeepConstraint.LOOKUP, KeepConstraint.NAME, KeepConstraint.FIELD_GET})
+public class MyClassWithFields implements PrintableFieldInterface {
+  final int intField = 42;
+  String stringField = "Hello!";
+}
+```
+
+
+Our use of [@UsedByReflection](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/UsedByReflection.html) is still not as flexible as the original [@UsesReflection](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/UsesReflection.html). In
+particular, if we change our code to no longer have any call to the library method
+`printFieldValues` the shrinker will still keep all of the fields on our annotated class.
+
+This is because the [@UsesReflection](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/UsesReflection.html) implicitly encodes as a precondition that the annotated
+method is actually used in the program. If not, the [@UsesReflection](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/UsesReflection.html) annotation is not
+"active".
+
+Luckily we can specify the same precondition using [@UsedByReflection.preconditions](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/UsedByReflection.html#preconditions()).
+
+
+```
+@UsedByReflection(
+    preconditions = {
+      @KeepCondition(
+          classConstant = FieldValuePrinterLibrary.class,
+          methodName = "printFieldValues")
+    },
+    kind = KeepItemKind.ONLY_FIELDS,
+    constraints = {KeepConstraint.LOOKUP, KeepConstraint.NAME, KeepConstraint.FIELD_GET})
+public class MyClassWithFields implements PrintableFieldInterface {
+  final int intField = 42;
+  String stringField = "Hello!";
+}
+```
+
+
+
+## Annotating APIs<a name="apis"></a>
+
+If your code is being shrunk before release as a library, or if you have an API
+surface that is used via dynamic loading at runtime, then you need to keep the
+API surface. For that you should use the [@KeepForApi](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepForApi.html) annotation.
+
+When annotating a class the default for [@KeepForApi](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepForApi.html) is to keep the class as well as all of its
+public and protected members:
+
+
+```
+@KeepForApi
+public class MyApi {
+  public void thisPublicMethodIsKept() {
+    /* ... */
+  }
+
+  protected void thisProtectedMethodIsKept() {
+    /* ... */
+  }
+
+  void thisPackagePrivateMethodIsNotKept() {
+    /* ... */
+  }
+
+  private void thisPrivateMethodIsNotKept() {
+    /* ... */
+  }
+}
+```
+
+
+The default can be changed using the [@KeepForApi.memberAccess](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepForApi.html#memberAccess()) property:
+
+
+```
+@KeepForApi(
+    memberAccess = {
+      MemberAccessFlags.PUBLIC,
+      MemberAccessFlags.PROTECTED,
+      MemberAccessFlags.PACKAGE_PRIVATE
+    })
+```
+
+
+The [@KeepForApi](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepForApi.html) annotation can also be placed directly on members and avoid keeping
+unannotated members. The holder class is implicitly kept. When annotating the members
+directly, the access does not matter as illustrated here by annotating a package private method:
+
+
+```
+public class MyOtherApi {
+
+  public void notKept() {
+    /* ... */
+  }
+
+  @KeepForApi
+  void isKept() {
+    /* ... */
+  }
+}
+```
+
+
+
+## Migrating rules to annotations<a name="migrating-rules"></a>
 
 There is no automatic migration of keep rules. Keep annotations often invert the
 direction and rules have no indication of where the reflection is taking
@@ -202,7 +338,7 @@
 
 
 
-## My use case is not covered!<a id="other-uses"></a>
+## My use case is not covered!<a name="other-uses"></a>
 
 The annotation library is in active development and not all use cases are
 described here or supported. Reach out to the R8 team by
@@ -210,7 +346,7 @@
 Describe your use case and we will look at how best to support it.
 
 
-## Troubleshooting<a id="troubleshooting"></a>
+## Troubleshooting<a name="troubleshooting"></a>
 
 If an annotation is not working as expected it may be helpful to inspect the
 rules that have been extracted for the annotation. This can be done by
diff --git a/doc/keepanno-guide.template.md b/doc/keepanno-guide.template.md
index 6e3efde..104bf02 100644
--- a/doc/keepanno-guide.template.md
+++ b/doc/keepanno-guide.template.md
@@ -86,12 +86,43 @@
 
 ## [Annotating code used by reflection (or via JNI)](used-by-reflection)
 
-TODO
+Sometimes reflecting code cannot be annotated. For example, the reflection can
+be done in native code or in a library outside your control. In such cases you
+can annotate the code that is being used by reflection with either
+`@UsedByReflection` or `@UsedByNative`. These two annotations are equivalent.
+Use the one that best matches why the annotation is needed.
+
+Let's consider some code with reflection outside our control.
+[[[INCLUDE DOC:UsedByReflectionFieldPrinterOnFields]]]
+
+[[[INCLUDE CODE:UsedByReflectionFieldPrinterOnFields]]]
+
+[[[INCLUDE DOC:UsedByReflectionFieldPrinterOnClass]]]
+
+[[[INCLUDE CODE:UsedByReflectionFieldPrinterOnClass]]]
+
+[[[INCLUDE DOC:UsedByReflectionFieldPrinterConditional]]]
+
+[[[INCLUDE CODE:UsedByReflectionFieldPrinterConditional]]]
 
 
 ## [Annotating APIs](apis)
 
-TODO
+If your code is being shrunk before release as a library, or if you have an API
+surface that is used via dynamic loading at runtime, then you need to keep the
+API surface. For that you should use the `@KeepForApi` annotation.
+
+[[[INCLUDE DOC:ApiClass]]]
+
+[[[INCLUDE CODE:ApiClass]]]
+
+[[[INCLUDE DOC:ApiClassMemberAccess]]]
+
+[[[INCLUDE CODE:ApiClassMemberAccess]]]
+
+[[[INCLUDE DOC:ApiMember]]]
+
+[[[INCLUDE CODE:ApiMember]]]
 
 
 ## [Migrating rules to annotations](migrating-rules)
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 b98f4ab..177990e 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
@@ -431,11 +431,27 @@
    * <p>If none, and other properties define this item as a field, the default matches any field
    * name.
    *
+   * <p>Mutually exclusive with the property `fieldNamePattern` also defining field-name.
+   *
    * @return The exact field name of the field.
    */
   String fieldName() default "";
 
   /**
+   * Define the field-name pattern by a string pattern.
+   *
+   * <p>Mutually exclusive with all method properties.
+   *
+   * <p>If none, and other properties define this item as a field, the default matches any field
+   * name.
+   *
+   * <p>Mutually exclusive with the property `fieldName` also defining field-name.
+   *
+   * @return The string pattern of the field name.
+   */
+  StringPattern fieldNamePattern() default @StringPattern(exact = "");
+
+  /**
    * Define the field-type pattern by a fully qualified type.
    *
    * <p>Mutually exclusive with all method properties.
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 bab5ca7..a35d08b 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
@@ -432,13 +432,37 @@
    * <p>If none, and other properties define this item as a field, the default matches any field
    * name.
    *
-   * <p>Mutually exclusive with the property `memberFromBinding` also defining field-name.
+   * <p>Mutually exclusive with the following other properties defining field-name:
+   *
+   * <ul>
+   *   <li>fieldNamePattern
+   *   <li>memberFromBinding
+   * </ul>
    *
    * @return The exact field name of the field.
    */
   String fieldName() default "";
 
   /**
+   * Define the field-name pattern by a string pattern.
+   *
+   * <p>Mutually exclusive with all method properties.
+   *
+   * <p>If none, and other properties define this item as a field, the default matches any field
+   * name.
+   *
+   * <p>Mutually exclusive with the following other properties defining field-name:
+   *
+   * <ul>
+   *   <li>fieldName
+   *   <li>memberFromBinding
+   * </ul>
+   *
+   * @return The string pattern of the field name.
+   */
+  StringPattern fieldNamePattern() default @StringPattern(exact = "");
+
+  /**
    * Define the field-type pattern by a fully qualified type.
    *
    * <p>Mutually exclusive with all method properties.
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 9d097f0..4b56b2e 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
@@ -210,11 +210,27 @@
    * <p>If none, and other properties define this item as a field, the default matches any field
    * name.
    *
+   * <p>Mutually exclusive with the property `fieldNamePattern` also defining field-name.
+   *
    * @return The exact field name of the field.
    */
   String fieldName() default "";
 
   /**
+   * Define the field-name pattern by a string pattern.
+   *
+   * <p>Mutually exclusive with all method properties.
+   *
+   * <p>If none, and other properties define this item as a field, the default matches any field
+   * name.
+   *
+   * <p>Mutually exclusive with the property `fieldName` also defining field-name.
+   *
+   * @return The string pattern of the field name.
+   */
+  StringPattern fieldNamePattern() default @StringPattern(exact = "");
+
+  /**
    * Define the field-type pattern by a fully qualified type.
    *
    * <p>Mutually exclusive with all method properties.
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 d601a6a..e2ebc3b 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
@@ -529,13 +529,37 @@
    * <p>If none, and other properties define this item as a field, the default matches any field
    * name.
    *
-   * <p>Mutually exclusive with the property `memberFromBinding` also defining field-name.
+   * <p>Mutually exclusive with the following other properties defining field-name:
+   *
+   * <ul>
+   *   <li>fieldNamePattern
+   *   <li>memberFromBinding
+   * </ul>
    *
    * @return The exact field name of the field.
    */
   String fieldName() default "";
 
   /**
+   * Define the field-name pattern by a string pattern.
+   *
+   * <p>Mutually exclusive with all method properties.
+   *
+   * <p>If none, and other properties define this item as a field, the default matches any field
+   * name.
+   *
+   * <p>Mutually exclusive with the following other properties defining field-name:
+   *
+   * <ul>
+   *   <li>fieldName
+   *   <li>memberFromBinding
+   * </ul>
+   *
+   * @return The string pattern of the field name.
+   */
+  StringPattern fieldNamePattern() default @StringPattern(exact = "");
+
+  /**
    * Define the field-type pattern by a fully qualified type.
    *
    * <p>Mutually exclusive with all method properties.
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 229d0f1..f025c0e 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
@@ -253,11 +253,27 @@
    * <p>If none, and other properties define this item as a field, the default matches any field
    * name.
    *
+   * <p>Mutually exclusive with the property `fieldNamePattern` also defining field-name.
+   *
    * @return The exact field name of the field.
    */
   String fieldName() default "";
 
   /**
+   * Define the field-name pattern by a string pattern.
+   *
+   * <p>Mutually exclusive with all method properties.
+   *
+   * <p>If none, and other properties define this item as a field, the default matches any field
+   * name.
+   *
+   * <p>Mutually exclusive with the property `fieldName` also defining field-name.
+   *
+   * @return The string pattern of the field name.
+   */
+  StringPattern fieldNamePattern() default @StringPattern(exact = "");
+
+  /**
    * Define the field-type pattern by a fully qualified type.
    *
    * <p>Mutually exclusive with all method properties.
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 7b8fa8e..8076e51 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
@@ -253,11 +253,27 @@
    * <p>If none, and other properties define this item as a field, the default matches any field
    * name.
    *
+   * <p>Mutually exclusive with the property `fieldNamePattern` also defining field-name.
+   *
    * @return The exact field name of the field.
    */
   String fieldName() default "";
 
   /**
+   * Define the field-name pattern by a string pattern.
+   *
+   * <p>Mutually exclusive with all method properties.
+   *
+   * <p>If none, and other properties define this item as a field, the default matches any field
+   * name.
+   *
+   * <p>Mutually exclusive with the property `fieldName` also defining field-name.
+   *
+   * @return The string pattern of the field name.
+   */
+  StringPattern fieldNamePattern() default @StringPattern(exact = "");
+
+  /**
    * Define the field-type pattern by a fully qualified type.
    *
    * <p>Mutually exclusive with all method properties.
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java
index 7f03de2..91751e6 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java
@@ -829,9 +829,6 @@
         if (!descriptor.equals(itemDescriptor)) {
           throw parsingContext.error("must reference its class context " + className);
         }
-        if (itemPattern.isMemberItemPattern() && items.size() == 1) {
-          throw parsingContext.error("kind must include its class");
-        }
         if (!holderPattern.getInstanceOfPattern().isAny()) {
           throw parsingContext.error("cannot define an 'extends' pattern.");
         }
@@ -1567,6 +1564,7 @@
   private static class FieldDeclaration extends Declaration<KeepFieldPattern> {
 
     private final ParsingContext parsingContext;
+    private final StringPatternParser nameParser;
     private final FieldTypeParser typeParser;
     private KeepFieldAccessPattern.Builder accessBuilder = null;
     private KeepFieldPattern.Builder builder = null;
@@ -1574,12 +1572,16 @@
 
     public FieldDeclaration(ParsingContext parsingContext) {
       this.parsingContext = parsingContext;
+      nameParser = new StringPatternParser(parsingContext.group(Item.fieldNameGroup));
+      nameParser.setProperty(Item.fieldName, StringProperty.EXACT);
+      nameParser.setProperty(Item.fieldNamePattern, StringProperty.PATTERN);
+
       typeParser = new FieldTypeParser(parsingContext.group(Item.fieldTypeGroup));
       typeParser.setProperty(Item.fieldTypePattern, TypeProperty.TYPE_PATTERN);
       typeParser.setProperty(Item.fieldType, TypeProperty.TYPE_NAME);
       typeParser.setProperty(Item.fieldTypeConstant, TypeProperty.TYPE_CONSTANT);
 
-      parsers = Collections.singletonList(typeParser);
+      parsers = ImmutableList.of(nameParser, typeParser);
     }
 
     @Override
@@ -1603,6 +1605,9 @@
       if (accessBuilder != null) {
         getBuilder().setAccessPattern(accessBuilder.build());
       }
+      if (!nameParser.isDefault()) {
+        getBuilder().setNamePattern(KeepFieldNamePattern.fromStringPattern(nameParser.getValue()));
+      }
       if (!typeParser.isDefault()) {
         getBuilder().setTypePattern(typeParser.getValue());
       }
@@ -1610,15 +1615,6 @@
     }
 
     @Override
-    boolean tryParse(String name, Object value) {
-      if (name.equals(Item.fieldName) && value instanceof String) {
-        getBuilder().setNamePattern(KeepFieldNamePattern.exact((String) value));
-        return true;
-      }
-      return super.tryParse(name, value);
-    }
-
-    @Override
     AnnotationVisitor tryParseArray(String name) {
       if (name.equals(Item.fieldAccess)) {
         accessBuilder = KeepFieldAccessPattern.builder();
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeWriter.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeWriter.java
index c714960..332e101 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeWriter.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeWriter.java
@@ -12,7 +12,6 @@
 import com.android.tools.r8.keepanno.ast.KeepConsequences;
 import com.android.tools.r8.keepanno.ast.KeepEdge;
 import com.android.tools.r8.keepanno.ast.KeepEdgeException;
-import com.android.tools.r8.keepanno.ast.KeepFieldNamePattern.KeepFieldNameExactPattern;
 import com.android.tools.r8.keepanno.ast.KeepFieldPattern;
 import com.android.tools.r8.keepanno.ast.KeepItemPattern;
 import com.android.tools.r8.keepanno.ast.KeepMemberItemPattern;
@@ -187,9 +186,9 @@
   }
 
   private void writeField(KeepFieldPattern field, AnnotationVisitor targetVisitor) {
-    KeepFieldNameExactPattern exactFieldName = field.getNamePattern().asExact();
+    String exactFieldName = field.getNamePattern().asExactString();
     if (exactFieldName != null) {
-      targetVisitor.visit(Item.fieldName, exactFieldName.getName());
+      targetVisitor.visit(Item.fieldName, exactFieldName);
     } else {
       throw new Unimplemented();
     }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/AnnotationConstants.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/AnnotationConstants.java
index c489701..f0bdccf 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/AnnotationConstants.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/AnnotationConstants.java
@@ -94,7 +94,9 @@
     public static final String methodParameters = "methodParameters";
     public static final String methodParameterTypePatterns = "methodParameterTypePatterns";
     public static final String fieldAccess = "fieldAccess";
+    public static final String fieldNameGroup = "field-name";
     public static final String fieldName = "fieldName";
+    public static final String fieldNamePattern = "fieldNamePattern";
     public static final String fieldTypeGroup = "field-type";
     public static final String fieldType = "fieldType";
     public static final String fieldTypeConstant = "fieldTypeConstant";
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepFieldNamePattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepFieldNamePattern.java
index 0945764..5223e60 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepFieldNamePattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepFieldNamePattern.java
@@ -3,96 +3,44 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.keepanno.ast;
 
-public abstract class KeepFieldNamePattern {
+public class KeepFieldNamePattern {
+
+  private static final KeepFieldNamePattern ANY = new KeepFieldNamePattern(KeepStringPattern.any());
 
   public static KeepFieldNamePattern any() {
-    return Any.getInstance();
+    return ANY;
   }
 
   public static KeepFieldNamePattern exact(String methodName) {
-    return new KeepFieldNameExactPattern(methodName);
+    return fromStringPattern(KeepStringPattern.exact(methodName));
   }
 
-  private KeepFieldNamePattern() {}
+  public static KeepFieldNamePattern fromStringPattern(KeepStringPattern pattern) {
+    if (pattern.isAny()) {
+      return ANY;
+    }
+    return new KeepFieldNamePattern(pattern);
+  }
+
+  private final KeepStringPattern pattern;
+
+  private KeepFieldNamePattern(KeepStringPattern pattern) {
+    this.pattern = pattern;
+  }
+
+  public KeepStringPattern asStringPattern() {
+    return pattern;
+  }
 
   public boolean isAny() {
-    return false;
+    return ANY == this;
   }
 
   public final boolean isExact() {
-    return asExact() != null;
+    return pattern.isExact();
   }
 
-  public KeepFieldNameExactPattern asExact() {
-    return null;
-  }
-
-  private static class Any extends KeepFieldNamePattern {
-    private static final Any INSTANCE = new Any();
-
-    public static Any getInstance() {
-      return INSTANCE;
-    }
-
-    @Override
-    public boolean isAny() {
-      return true;
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-      return this == obj;
-    }
-
-    @Override
-    public int hashCode() {
-      return System.identityHashCode(this);
-    }
-
-    @Override
-    public String toString() {
-      return "*";
-    }
-  }
-
-  public static class KeepFieldNameExactPattern extends KeepFieldNamePattern {
-    private final String name;
-
-    public KeepFieldNameExactPattern(String name) {
-      assert name != null;
-      this.name = name;
-    }
-
-    @Override
-    public KeepFieldNameExactPattern asExact() {
-      return this;
-    }
-
-    public String getName() {
-      return name;
-    }
-
-    @Override
-    @SuppressWarnings("EqualsGetClass")
-    public boolean equals(Object o) {
-      if (this == o) {
-        return true;
-      }
-      if (o == null || getClass() != o.getClass()) {
-        return false;
-      }
-      KeepFieldNameExactPattern that = (KeepFieldNameExactPattern) o;
-      return name.equals(that.name);
-    }
-
-    @Override
-    public int hashCode() {
-      return name.hashCode();
-    }
-
-    @Override
-    public String toString() {
-      return name;
-    }
+  public String asExactString() {
+    return pattern.asExactString();
   }
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodNamePattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodNamePattern.java
index 6843e61..b79b616 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodNamePattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodNamePattern.java
@@ -3,17 +3,27 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.keepanno.ast;
 
+public class KeepMethodNamePattern {
 
-public abstract class KeepMethodNamePattern {
   private static final String INIT_STRING = "<init>";
   private static final String CLINIT_STRING = "<clinit>";
+  private static final KeepMethodNamePattern ANY =
+      new KeepMethodNamePattern(KeepStringPattern.any());
+  private static final KeepMethodNamePattern INSTANCE_INIT =
+      new KeepMethodNamePattern(KeepStringPattern.exact(INIT_STRING));
+  private static final KeepMethodNamePattern CLASS_INIT =
+      new KeepMethodNamePattern(KeepStringPattern.exact(CLINIT_STRING));
 
   public static KeepMethodNamePattern any() {
-    return SomePattern.ANY;
+    return KeepMethodNamePattern.ANY;
   }
 
-  public static KeepMethodNamePattern initializer() {
-    return SomePattern.INSTANCE_INIT;
+  public static KeepMethodNamePattern instanceInitializer() {
+    return KeepMethodNamePattern.INSTANCE_INIT;
+  }
+
+  public static KeepMethodNamePattern classInitializer() {
+    return KeepMethodNamePattern.CLASS_INIT;
   }
 
   public static KeepMethodNamePattern exact(String methodName) {
@@ -22,98 +32,70 @@
 
   public static KeepMethodNamePattern fromStringPattern(KeepStringPattern pattern) {
     if (pattern.isAny()) {
-      return SomePattern.ANY;
+      return KeepMethodNamePattern.ANY;
     }
     if (pattern.isExact()) {
       String exact = pattern.asExactString();
       if (INIT_STRING.equals(exact)) {
-        return SomePattern.INSTANCE_INIT;
+        return KeepMethodNamePattern.INSTANCE_INIT;
       }
       if (CLINIT_STRING.equals(exact)) {
-        return SomePattern.CLASS_INIT;
+        return KeepMethodNamePattern.CLASS_INIT;
       }
     }
-    return new SomePattern(pattern);
+    return new KeepMethodNamePattern(pattern);
   }
 
-  private KeepMethodNamePattern() {}
+  private final KeepStringPattern pattern;
 
-  public abstract boolean isAny();
+  private KeepMethodNamePattern(KeepStringPattern pattern) {
+    assert pattern != null;
+    this.pattern = pattern;
+  }
 
-  public abstract boolean isInstanceInitializer();
+  public KeepStringPattern asStringPattern() {
+    return pattern;
+  }
 
-  public abstract boolean isClassInitializer();
-
-  public abstract boolean isExact();
-
-  public abstract String asExactString();
-
-  public abstract KeepStringPattern asStringPattern();
-
-  private static class SomePattern extends KeepMethodNamePattern {
-    private static final SomePattern ANY = new SomePattern(KeepStringPattern.any());
-    private static final KeepMethodNamePattern INSTANCE_INIT =
-        new SomePattern(KeepStringPattern.exact("<init>"));
-    private static final KeepMethodNamePattern CLASS_INIT =
-        new SomePattern(KeepStringPattern.exact("<clinit>"));
-
-    private final KeepStringPattern pattern;
-
-    public SomePattern(KeepStringPattern pattern) {
-      assert pattern != null;
-      this.pattern = pattern;
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
     }
-
-    @Override
-    public KeepStringPattern asStringPattern() {
-      return pattern;
+    if (!(o instanceof KeepMethodNamePattern)) {
+      return false;
     }
+    KeepMethodNamePattern that = (KeepMethodNamePattern) o;
+    return pattern.equals(that.pattern);
+  }
 
-    @Override
-    public boolean equals(Object o) {
-      if (this == o) {
-        return true;
-      }
-      if (!(o instanceof SomePattern)) {
-        return false;
-      }
-      SomePattern that = (SomePattern) o;
-      return pattern.equals(that.pattern);
-    }
+  @Override
+  public int hashCode() {
+    return pattern.hashCode();
+  }
 
-    @Override
-    public int hashCode() {
-      return pattern.hashCode();
-    }
+  @Override
+  public String toString() {
+    return pattern.toString();
+  }
 
-    @Override
-    public String toString() {
-      return pattern.toString();
-    }
+  public boolean isAny() {
+    return ANY == this;
+  }
 
-    @Override
-    public boolean isAny() {
-      return ANY == this;
-    }
+  public boolean isClassInitializer() {
+    return CLASS_INIT == this;
+  }
 
-    @Override
-    public boolean isClassInitializer() {
-      return CLASS_INIT == this;
-    }
+  public boolean isInstanceInitializer() {
+    return INSTANCE_INIT == this;
+  }
 
-    @Override
-    public boolean isInstanceInitializer() {
-      return INSTANCE_INIT == this;
-    }
+  public boolean isExact() {
+    return pattern.isExact();
+  }
 
-    @Override
-    public boolean isExact() {
-      return pattern.isExact();
-    }
-
-    @Override
-    public String asExactString() {
-      return pattern.asExactString();
-    }
+  public String asExactString() {
+    return pattern.asExactString();
   }
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/RulePrintingUtils.java b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/RulePrintingUtils.java
index f16d9dd..7737744 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/RulePrintingUtils.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/RulePrintingUtils.java
@@ -176,9 +176,7 @@
   }
 
   private static RulePrinter printFieldName(RulePrinter builder, KeepFieldNamePattern namePattern) {
-    return namePattern.isAny()
-        ? builder.appendStar()
-        : builder.append(namePattern.asExact().getName());
+    return printStringPattern(builder, namePattern.asStringPattern());
   }
 
   private static RulePrinter printMethodName(
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index ad074ed..32d16c3 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -907,7 +907,9 @@
           }
         }
       }
-
+      if (options.androidResourceProguardMapStrings != null) {
+        resourceShrinkerBuilder.setProguardMapStrings(options.androidResourceProguardMapStrings);
+      }
       LegacyResourceShrinker shrinker = resourceShrinkerBuilder.build();
       ShrinkerResult shrinkerResult;
       if (options.resourceShrinkerConfiguration.isOptimizedShrinking()) {
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 405f214..2b20068 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -1177,13 +1177,21 @@
     MapConsumer mapConsumer =
         wrapExistingMapConsumerIfNotNull(
             internal.mapConsumer, partitionMapConsumer, MapConsumerToPartitionMapConsumer::new);
-    internal.mapConsumer =
+    mapConsumer =
         wrapExistingMapConsumerIfNotNull(
             mapConsumer,
             stringConsumer,
             nonNullStringConsumer ->
                 ProguardMapStringConsumer.builder().setStringConsumer(stringConsumer).build());
 
+    internal.mapConsumer =
+        wrapExistingMapConsumerIfNotNull(
+            mapConsumer,
+            androidResourceConsumer,
+            nonNulStringConsumer ->
+                ProguardMapStringConsumer.builder()
+                    .setStringConsumer(new ResourceShrinkerMapStringConsumer(internal))
+                    .build());
     // Amend the usage information consumer with options from the proguard configuration.
     internal.usageInformationConsumer =
         wrapStringConsumer(
@@ -1308,6 +1316,27 @@
     return optionConsumer;
   }
 
+  private static class ResourceShrinkerMapStringConsumer implements StringConsumer {
+
+    private final InternalOptions internal;
+    private StringBuilder resultBuilder = new StringBuilder();
+
+    public ResourceShrinkerMapStringConsumer(InternalOptions internal) {
+      this.internal = internal;
+    }
+
+    @Override
+    public void accept(String string, DiagnosticsHandler handler) {
+      resultBuilder.append(string);
+    }
+
+    @Override
+    public void finished(DiagnosticsHandler handler) {
+      internal.androidResourceProguardMapStrings = StringUtils.splitLines(resultBuilder.toString());
+      resultBuilder = null;
+    }
+  }
+
   private static class StandardOutConsumer extends StringConsumer.ForwardingConsumer {
 
     public StandardOutConsumer(StringConsumer consumer) {
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java b/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java
index d26c44c..2d8cc3e 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java
@@ -244,6 +244,10 @@
     return get(Opcodes.MUL);
   }
 
+  public boolean mayHaveNewArrayEmpty() {
+    return get(Opcodes.NEW_ARRAY_EMPTY);
+  }
+
   public boolean mayHaveNewArrayFilled() {
     return get(Opcodes.NEW_ARRAY_FILLED);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/ArrayConstructionSimplifier.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/ArrayConstructionSimplifier.java
index 2a93788..df8d38a 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/ArrayConstructionSimplifier.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/ArrayConstructionSimplifier.java
@@ -96,15 +96,12 @@
       BasicBlock block = worklist.next();
       hasChanged |= simplifyArrayConstructionBlock(block, worklist, code, appView.options());
     }
-    if (hasChanged) {
-      code.removeRedundantBlocks();
-    }
     return CodeRewriterResult.hasChanged(hasChanged);
   }
 
   @Override
   protected boolean shouldRewriteCode(IRCode code) {
-    return true;
+    return code.metadata().mayHaveNewArrayEmpty();
   }
 
   private boolean simplifyArrayConstructionBlock(
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/FilledNewArrayRewriter.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/FilledNewArrayRewriter.java
index e269dc1..28a40a5 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/FilledNewArrayRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/FilledNewArrayRewriter.java
@@ -31,11 +31,14 @@
 import com.android.tools.r8.ir.code.StaticGet;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
+import com.android.tools.r8.utils.BooleanBox;
 import com.android.tools.r8.utils.InternalOptions.RewriteArrayOptions;
 import com.android.tools.r8.utils.SetUtils;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
+import java.util.ArrayList;
 import java.util.IdentityHashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
@@ -61,31 +64,63 @@
   protected CodeRewriterResult rewriteCode(IRCode code) {
     assert !mayHaveRedundantBlocks;
     assert toRemove == NOTHING;
-    BasicBlockIterator blockIterator = code.listIterator();
     CodeRewriterResult result = noChange();
-    while (blockIterator.hasNext()) {
-      BasicBlock block = blockIterator.next();
-      BasicBlockInstructionListIterator instructionIterator = block.listIterator(code);
-      while (instructionIterator.hasNext()) {
-        Instruction instruction = instructionIterator.next();
-        if (instruction.isNewArrayFilled()) {
-          result =
-              processInstruction(
-                  code, blockIterator, instructionIterator, instruction.asNewArrayFilled(), result);
+    BooleanBox pendingRewrites = new BooleanBox(true);
+    while (pendingRewrites.get()) {
+      pendingRewrites.set(false);
+      BasicBlockIterator blockIterator = code.listIterator();
+      while (blockIterator.hasNext()) {
+        BasicBlock block = blockIterator.next();
+        BasicBlockInstructionListIterator instructionIterator = block.listIterator(code);
+        while (instructionIterator.hasNext()) {
+          Instruction instruction = instructionIterator.next();
+          if (instruction.isNewArrayFilled()) {
+            result =
+                processInstruction(
+                    code,
+                    blockIterator,
+                    instructionIterator,
+                    instruction.asNewArrayFilled(),
+                    result,
+                    pendingRewrites);
+          }
         }
       }
-    }
-    if (!toRemove.isEmpty()) {
-      InstructionListIterator it = code.instructionListIterator();
-      while (it.hasNext()) {
-        if (toRemove.contains(it.next())) {
-          it.remove();
-          mayHaveRedundantBlocks = true;
+      if (!toRemove.isEmpty()) {
+        Set<Instruction> additionalToRemove = SetUtils.newIdentityHashSet();
+        InstructionListIterator it = code.instructionListIterator();
+        while (it.hasNext()) {
+          Instruction next = it.next();
+          if (toRemove.contains(next)) {
+            // Also remove constants used by the removed NewArrayFilled.
+            if (next.isNewArrayFilled()) {
+              next.inValues()
+                  .forEach(
+                      value -> {
+                        if (value.hasSingleUniqueUser()) {
+                          additionalToRemove.add(value.getDefinition());
+                        }
+                      });
+            }
+            it.remove();
+            mayHaveRedundantBlocks = true;
+          }
+        }
+        if (!additionalToRemove.isEmpty()) {
+          InstructionListIterator itAdditional = code.instructionListIterator();
+          while (itAdditional.hasNext()) {
+            Instruction next = itAdditional.next();
+            if (additionalToRemove.contains(next)) {
+              itAdditional.remove();
+              mayHaveRedundantBlocks = true;
+            }
+          }
         }
       }
-    }
-    if (mayHaveRedundantBlocks) {
-      code.removeRedundantBlocks();
+      toRemove = NOTHING;
+      if (mayHaveRedundantBlocks) {
+        code.removeRedundantBlocks();
+      }
     }
     return result;
   }
@@ -95,12 +130,49 @@
     return code.metadata().mayHaveNewArrayFilled();
   }
 
+  private boolean isNewArrayFilledOfConstants(NewArrayFilled newArrayFilled) {
+    for (Value inValue : newArrayFilled.inValues()) {
+      if (!inValue.isConstNumber() && !inValue.isConstString() && !inValue.isConstClass()) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  private boolean isDefinedByNewArrayFilledOfConstants(Value value) {
+    if (!value.isDefinedByInstructionSatisfying(Instruction::isNewArrayFilled)) {
+      return false;
+    }
+    return isNewArrayFilledOfConstants(value.definition.asNewArrayFilled());
+  }
+
+  public NewArrayFilled copyConstantsNewArrayFilled(IRCode code, NewArrayFilled original) {
+    assert isNewArrayFilledOfConstants(original);
+    Value newValue = code.createValue(original.getOutType(), original.getLocalInfo());
+    List<Value> newArguments = new ArrayList<>(original.inValues().size());
+    for (Value value : original.inValues()) {
+      if (value.isConstNumber()) {
+        newArguments.add(
+            ConstNumber.copyOf(code, value.getDefinition().asConstNumber()).outValue());
+      } else if (value.isConstString()) {
+        newArguments.add(
+            ConstString.copyOf(code, value.getDefinition().asConstString()).outValue());
+      } else if (value.isConstClass()) {
+        newArguments.add(ConstClass.copyOf(code, value.getDefinition().asConstClass()).outValue());
+      } else {
+        assert false;
+      }
+    }
+    return new NewArrayFilled(original.getArrayType(), newValue, newArguments);
+  }
+
   private CodeRewriterResult processInstruction(
       IRCode code,
       BasicBlockIterator blockIterator,
       BasicBlockInstructionListIterator instructionIterator,
       NewArrayFilled newArrayFilled,
-      CodeRewriterResult result) {
+      CodeRewriterResult result,
+      BooleanBox pendingRewrites) {
     if (canUseNewArrayFilled(newArrayFilled)) {
       return result;
     }
@@ -108,13 +180,59 @@
       instructionIterator.removeOrReplaceByDebugLocalRead();
     } else if (canUseNewArrayFilledData(newArrayFilled)) {
       rewriteToNewArrayFilledData(code, blockIterator, instructionIterator, newArrayFilled);
+    } else if (newArrayFilled.outValue().hasSingleUniqueUser()
+        && newArrayFilled.outValue().singleUniqueUser().isNewArrayFilled()
+        && isNewArrayFilledOfConstants(newArrayFilled)) {
+      if (canUseNewArrayFilled(newArrayFilled.outValue().singleUniqueUser().asNewArrayFilled())) {
+        // The NewArrayFilled user is supported, so rewrite here.
+        rewriteToArrayPuts(code, blockIterator, instructionIterator, newArrayFilled);
+      } else {
+        // The NewArrayFilled user is not supported so leave for rewriting after that.
+        //
+        // The effect of this is that when the user of this NewArrayFilled is rewritten to puts,
+        // the NewArrayFilled construction is copied to the use site
+        //
+        //  Input:
+        //
+        //   v0 <-  Const X
+        //   v1 <-  NewArrayFilled(v0)
+        //   v2 <-  Const Y
+        //   v3 <-  NewArrayFilled(v2)
+        //   v4 <-  NewArrayFilled(v1, v3)
+        //
+        // After rewriting the user (v0 - v3 are unused and removed):
+        //
+        //   v4 <-  NewArrayEmpty(...)
+        //   v5 <-  Const X
+        //   v6 <-  NewArrayFilled(v5)
+        //          APut v4, <Const 0>, v6
+        //   v7 <-  Const Y
+        //   v8 <-  NewArrayFilled(v7)
+        //          APut v4, <Const 1>, v8
+        //
+        // Setting pending rewrites cause the copied NewArrayFilled to be rewritten in their new
+        // location in the fixpoint:
+        //
+        //   v4 <-  NewArrayEmpty(...)
+        //   v9 <-  NewArrayEmpty(...)
+        //   v10 <- Const X
+        //          APut v9, <Const 0>, v10
+        //          APut v4, <Const 0>, v9
+        //   v11 <- NewArrayEmpty(...)
+        //   v12 <- Const Y
+        //          APut v11, <Const 0>, v12
+        //          APut v4, <Const 1>, v11
+        //
+        // If the NewArrayFilled which gets moved is supported then the second rewriting in the
+        // fixpoint does not happen.
+        pendingRewrites.set(true);
+      }
     } else {
       rewriteToArrayPuts(code, blockIterator, instructionIterator, newArrayFilled);
     }
     return CodeRewriterResult.HAS_CHANGED;
   }
 
-  @SuppressWarnings("ReferenceEquality")
   private boolean canUseNewArrayFilled(NewArrayFilled newArrayFilled) {
     if (!options.isGeneratingDex()) {
       return false;
@@ -125,7 +243,7 @@
     }
     // filled-new-array is implemented only for int[] and Object[].
     DexType arrayType = newArrayFilled.getArrayType();
-    if (arrayType == dexItemFactory.intArrayType) {
+    if (arrayType.isIdenticalTo(dexItemFactory.intArrayType)) {
       // For int[], using filled-new-array is usually smaller than filled-array-data.
       // filled-new-array supports up to 5 registers before it's filled-new-array/range.
       if (size > rewriteArrayOptions.maxSizeForFilledNewArrayOfInts) {
@@ -143,7 +261,7 @@
       if (size > rewriteArrayOptions.maxSizeForFilledNewArrayOfReferences) {
         return false;
       }
-      if (arrayType == dexItemFactory.stringArrayType) {
+      if (arrayType.isIdenticalTo(dexItemFactory.stringArrayType)) {
         return rewriteArrayOptions.canUseFilledNewArrayOfStrings();
       }
       if (!rewriteArrayOptions.canUseFilledNewArrayOfNonStringObjects()) {
@@ -155,7 +273,7 @@
       }
       // Check that all arguments to the array is the array type or that the array is type Object[].
       if (rewriteArrayOptions.canHaveSubTypesInFilledNewArrayBug()
-          && arrayType != dexItemFactory.objectArrayType
+          && arrayType.isNotIdenticalTo(dexItemFactory.objectArrayType)
           && !arrayType.isPrimitiveArrayType()) {
         DexType arrayElementType = arrayType.toArrayElementType(dexItemFactory);
         for (Value elementValue : newArrayFilled.inValues()) {
@@ -169,9 +287,8 @@
     return false;
   }
 
-  @SuppressWarnings("ReferenceEquality")
   private boolean canStoreElementInNewArrayFilled(TypeElement valueType, DexType elementType) {
-    if (elementType == dexItemFactory.objectType) {
+    if (elementType.isIdenticalTo(dexItemFactory.objectType)) {
       return true;
     }
     if (valueType.isNullType() && !elementType.isPrimitiveType()) {
@@ -512,7 +629,9 @@
         || !(elementValue.isConstString()
             || elementValue.isConstNumber()
             || elementValue.isConstClass()
-            || elementValue.isDefinedByInstructionSatisfying(Instruction::isStaticGet))) {
+            || elementValue.isDefinedByInstructionSatisfying(Instruction::isStaticGet)
+            || (isDefinedByNewArrayFilledOfConstants(elementValue)
+                && !instructionIterator.getBlock().hasCatchHandlers()))) {
       return elementValue;
     }
 
@@ -534,6 +653,14 @@
     } else if (elementValue.isDefinedByInstructionSatisfying(Instruction::isStaticGet)) {
       copy = StaticGet.copyOf(code, elementValue.getDefinition().asStaticGet());
       constantMaterializingInstructionCache.putNewValue(copy.asStaticGet().outValue());
+    } else if (isDefinedByNewArrayFilledOfConstants(elementValue)) {
+      copy = copyConstantsNewArrayFilled(code, elementValue.getDefinition().asNewArrayFilled());
+      assert !instructionIterator.getBlock().hasCatchHandlers();
+      for (Value inValue : copy.asNewArrayFilled().inValues()) {
+        instructionIterator.add(inValue.getDefinition());
+        inValue.getDefinition().setBlock(instructionIterator.getBlock());
+        inValue.getDefinition().setPosition(newArrayEmpty.getPosition());
+      }
     } else {
       assert false;
       return elementValue;
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java b/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java
index 26d9e87..8100e4e 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java
@@ -5,6 +5,7 @@
 
 import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
 import static com.android.tools.r8.utils.MapUtils.ignoreKey;
+import static com.google.common.base.Predicates.alwaysFalse;
 
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
@@ -16,6 +17,7 @@
 import com.android.tools.r8.graph.DexEncodedMember;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexItem;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexReference;
@@ -28,15 +30,22 @@
 import com.android.tools.r8.graph.lens.NonIdentityGraphLens;
 import com.android.tools.r8.shaking.KeepFieldInfo.Joiner;
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.MapUtils;
 import com.android.tools.r8.utils.Timing;
 import com.google.common.collect.Streams;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
 import java.util.IdentityHashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.BiConsumer;
 import java.util.function.Consumer;
 import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.function.Supplier;
 
 // Non-mutable collection of keep information pertaining to a program.
@@ -253,6 +262,8 @@
 
   public abstract KeepInfoCollection mutate(Consumer<MutableKeepInfoCollection> mutator);
 
+  public abstract void writeToDirectory(Path directory) throws IOException;
+
   // Mutation interface for building up the keep info.
   public static class MutableKeepInfoCollection extends KeepInfoCollection {
 
@@ -611,5 +622,53 @@
             }
           });
     }
+
+    @Override
+    public void writeToDirectory(Path directory) throws IOException {
+      writePropertyToFile(
+          directory.resolve("no-horizontal-class-merging.txt"),
+          classInfo -> !classInfo.internalIsHorizontalClassMergingAllowed(),
+          alwaysFalse(),
+          alwaysFalse());
+      writePropertyToFile(
+          directory.resolve("no-optimization.txt"),
+          classInfo -> !classInfo.internalIsOptimizationAllowed(),
+          fieldInfo -> !fieldInfo.internalIsOptimizationAllowed(),
+          methodInfo -> !methodInfo.internalIsOptimizationAllowed());
+      writePropertyToFile(
+          directory.resolve("no-shrinking.txt"),
+          classInfo -> !classInfo.internalIsShrinkingAllowed(),
+          fieldInfo -> !fieldInfo.internalIsShrinkingAllowed(),
+          methodInfo -> !methodInfo.internalIsShrinkingAllowed());
+    }
+
+    private void writePropertyToFile(
+        Path path,
+        Predicate<KeepClassInfo> classInfoPredicate,
+        Predicate<KeepFieldInfo> fieldInfoPredicate,
+        Predicate<KeepMethodInfo> methodInfoPredicate)
+        throws IOException {
+      List<DexReference> lines = new ArrayList<>();
+      keepClassInfo.forEach(
+          (clazz, info) -> {
+            if (classInfoPredicate.test(info)) {
+              lines.add(clazz);
+            }
+          });
+      keepFieldInfo.forEach(
+          (field, info) -> {
+            if (fieldInfoPredicate.test(info)) {
+              lines.add(field);
+            }
+          });
+      keepMethodInfo.forEach(
+          (method, info) -> {
+            if (methodInfoPredicate.test(info)) {
+              lines.add(method);
+            }
+          });
+      lines.sort(DexReference::compareTo);
+      Files.write(path, ListUtils.map(lines, DexItem::toSourceString));
+    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardIfRule.java b/src/main/java/com/android/tools/r8/shaking/ProguardIfRule.java
index 79b6f5f..2279a8e 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardIfRule.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardIfRule.java
@@ -253,7 +253,6 @@
     if (getMemberRules() != null) {
       memberRules =
           getMemberRules().stream()
-              .filter(rule -> rule.getRuleType().includesMethods())
               .map(memberRule -> memberRule.materialize(dexItemFactory))
               .collect(Collectors.toList());
     }
diff --git a/src/main/java/com/android/tools/r8/shaking/TreePruner.java b/src/main/java/com/android/tools/r8/shaking/TreePruner.java
index 7dc92f2..d13b3d0 100644
--- a/src/main/java/com/android/tools/r8/shaking/TreePruner.java
+++ b/src/main/java/com/android/tools/r8/shaking/TreePruner.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.shaking;
 
+import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
 import static com.google.common.base.Predicates.alwaysFalse;
 
 import com.android.tools.r8.androidapi.ComputedApiLevel;
@@ -154,8 +155,9 @@
       retainReachableInterfacesFrom(type, reachableInterfaces);
     }
     if (!reachableInterfaces.isEmpty()) {
+      boolean pinnedHolder = !appView.getKeepInfo(clazz).isOptimizationAllowed(appView.options());
       removeInterfacesImplementedDirectlyAndIndirectlyByClassFromSet(
-          clazz.superType, reachableInterfaces);
+          pinnedHolder, clazz.superType, reachableInterfaces, clazz);
     }
     if (reachableInterfaces.isEmpty()) {
       clazz.interfaces = DexTypeList.empty();
@@ -165,7 +167,7 @@
   }
 
   private void removeInterfacesImplementedDirectlyAndIndirectlyByClassFromSet(
-      DexType type, Set<DexType> interfaces) {
+      boolean pinnedRoot, DexType type, Set<DexType> interfaces, DexProgramClass context) {
     DexClass clazz = appView.definitionFor(type);
     if (clazz == null) {
       return;
@@ -176,13 +178,22 @@
       return;
     }
     for (DexType itf : clazz.interfaces) {
+      if (pinnedRoot) {
+        DexProgramClass itfClass = asProgramClassOrNull(appView.definitionFor(itf, context));
+        if (itfClass == null
+            || !appView.getKeepInfo(itfClass).isOptimizationAllowed(appView.options())) {
+          // If root-holder and interface are pinned then retain the interface on the root-holder.
+          continue;
+        }
+      }
       if (interfaces.remove(itf) && interfaces.isEmpty()) {
         return;
       }
     }
     if (clazz.superType != null) {
       assert !interfaces.isEmpty();
-      removeInterfacesImplementedDirectlyAndIndirectlyByClassFromSet(clazz.superType, interfaces);
+      removeInterfacesImplementedDirectlyAndIndirectlyByClassFromSet(
+          pinnedRoot, clazz.superType, interfaces, context);
     }
   }
 
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 26a4087..011fbfb 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -201,6 +201,8 @@
   public CancelCompilationChecker cancelCompilationChecker = null;
   public AndroidResourceProvider androidResourceProvider = null;
   public AndroidResourceConsumer androidResourceConsumer = null;
+  public List<String> androidResourceProguardMapStrings = null;
+
   public ResourceShrinkerConfiguration resourceShrinkerConfiguration =
       ResourceShrinkerConfiguration.DEFAULT_CONFIGURATION;
   public ResourceAccessAnalysis resourceAccessAnalysis = null;
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/obfuscation/ProguardMappingsRecorder.kt b/src/resourceshrinker/java/com/android/build/shrinker/obfuscation/ProguardMappingsRecorder.kt
index dd538c7..2e2aaca 100644
--- a/src/resourceshrinker/java/com/android/build/shrinker/obfuscation/ProguardMappingsRecorder.kt
+++ b/src/resourceshrinker/java/com/android/build/shrinker/obfuscation/ProguardMappingsRecorder.kt
@@ -24,9 +24,12 @@
 /**
  * Records obfuscation mappings from single file in proguard format.
  *
- * @param mappingsFile path to proguard.map file.
+ * @param mapping file lines or the mapping file path.
  */
-class ProguardMappingsRecorder(private val mappingsFile: Path) : ObfuscationMappingsRecorder {
+class ProguardMappingsRecorder(private val mappingLines: List<String>) : ObfuscationMappingsRecorder {
+
+    constructor(mappingsFile: Path)
+      : this(Files.readAllLines(mappingsFile, StandardCharsets.UTF_8));
 
     override fun recordObfuscationMappings(model: ResourceShrinkerModel) {
         model.obfuscatedClasses = extractObfuscatedResourceClasses()
@@ -41,9 +44,8 @@
         // com.package.R -> a.b:
         // com.package.R$style -> a.b.a:
         //     int Toolbar_android_gravity -> i1
-
         val builder = ObfuscatedClasses.Builder()
-        Files.readAllLines(mappingsFile, StandardCharsets.UTF_8).forEach { line ->
+        mappingLines.forEach { line ->
             when {
                 isMethodMapping(line) -> builder.addMethodMapping(extractMethodMapping(line))
                 isClassMapping(line) -> builder.addClassMapping(extractClassMapping(line))
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/LegacyResourceShrinker.java b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/LegacyResourceShrinker.java
index ad7ddfb..1c0a875 100644
--- a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/LegacyResourceShrinker.java
+++ b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/LegacyResourceShrinker.java
@@ -15,6 +15,7 @@
 import com.android.build.shrinker.ResourceTableUtilKt;
 import com.android.build.shrinker.graph.ProtoResourcesGraphBuilder;
 import com.android.build.shrinker.graph.ResFolderFileTree;
+import com.android.build.shrinker.obfuscation.ProguardMappingsRecorder;
 import com.android.build.shrinker.r8integration.R8ResourceShrinkerState.R8ResourceShrinkerModel;
 import com.android.build.shrinker.usages.DexFileAnalysisCallback;
 import com.android.build.shrinker.usages.ProtoAndroidManifestUsageRecorderKt;
@@ -48,6 +49,7 @@
   private final Map<String, byte[]> dexInputs;
   private final List<PathAndBytes> resFolderInputs;
   private final List<PathAndBytes> xmlInputs;
+  private List<String> proguardMapStrings;
   private final List<PathAndBytes> manifest;
   private final Map<PathAndBytes, FeatureSplit> resourceTables;
 
@@ -59,6 +61,7 @@
 
     private final List<PathAndBytes> manifests = new ArrayList<>();
     private final Map<PathAndBytes, FeatureSplit> resourceTables = new HashMap<>();
+    private List<String> proguardMapStrings;
 
     private Builder() {}
 
@@ -96,7 +99,11 @@
     public LegacyResourceShrinker build() {
       assert manifests != null && resourceTables != null;
       return new LegacyResourceShrinker(
-          dexInputs, resFolderInputs, manifests, resourceTables, xmlInputs);
+          dexInputs, resFolderInputs, manifests, resourceTables, xmlInputs, proguardMapStrings);
+    }
+
+    public void setProguardMapStrings(List<String> proguardMapStrings) {
+      this.proguardMapStrings = proguardMapStrings;
     }
   }
 
@@ -105,12 +112,14 @@
       List<PathAndBytes> resFolderInputs,
       List<PathAndBytes> manifests,
       Map<PathAndBytes, FeatureSplit> resourceTables,
-      List<PathAndBytes> xmlInputs) {
+      List<PathAndBytes> xmlInputs,
+      List<String> proguardMapStrings) {
     this.dexInputs = dexInputs;
     this.resFolderInputs = resFolderInputs;
     this.manifest = manifests;
     this.resourceTables = resourceTables;
     this.xmlInputs = xmlInputs;
+    this.proguardMapStrings = proguardMapStrings;
   }
 
   public static Builder builder() {
@@ -127,6 +136,10 @@
   }
 
   public ShrinkerResult shrinkModel(R8ResourceShrinkerModel model) throws IOException {
+    if (proguardMapStrings != null) {
+      new ProguardMappingsRecorder(proguardMapStrings).recordObfuscationMappings(model);
+      proguardMapStrings = null;
+    }
     for (Entry<String, byte[]> entry : dexInputs.entrySet()) {
       // The analysis needs an origin for the dex files, synthesize an easy recognizable one.
       Path inMemoryR8 = Paths.get("in_memory_r8_" + entry.getKey() + ".dex");
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index 9d47152..729f5e7 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -1787,6 +1787,20 @@
             ToolHelper.getDexVersionForApiLevel(apiLevelWithJavaTime()));
   }
 
+  public boolean canUseFilledNewArrayOfInteger(TestParameters parameters) {
+    return parameters.isDexRuntime();
+  }
+
+  public boolean canUseFilledNewArrayOfStringObjects(TestParameters parameters) {
+    return parameters.isDexRuntime()
+        && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.K);
+  }
+
+  public boolean canUseFilledNewArrayOfNonStringObjects(TestParameters parameters) {
+    return parameters.isDexRuntime()
+        && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.N);
+  }
+
   // TODO(b/131130038): Do not allow accessmodification when kept.
   public boolean isForceAccessModifyingPackagePrivateAndProtectedMethods() {
     return true;
diff --git a/src/test/java/com/android/tools/r8/androidresources/RClassStaticValuesIgnoreTest.java b/src/test/java/com/android/tools/r8/androidresources/RClassStaticValuesIgnoreTest.java
new file mode 100644
index 0000000..fdc8a87
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/androidresources/RClassStaticValuesIgnoreTest.java
@@ -0,0 +1,91 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.androidresources;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResource;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResourceBuilder;
+import com.android.tools.r8.utils.DescriptorUtils;
+import java.io.IOException;
+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 RClassStaticValuesIgnoreTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  private static final String RClassDescriptor =
+      descriptor(RClassStaticValuesIgnoreTest.class)
+          .replace(RClassStaticValuesIgnoreTest.class.getSimpleName(), "R$string");
+
+  @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)
+        .build(temp);
+  }
+
+  private byte[] getRClassWithReferenceToUnused() throws IOException {
+    return transformer(ToBeRenamedToRDollarString.class)
+        .setClassDescriptor(RClassDescriptor)
+        .transform();
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    AndroidTestResource testResources = getTestResources(temp);
+    testForR8(parameters.getBackend())
+        .setMinApi(parameters)
+        .addProgramClasses(FooBar.class)
+        .addProgramClassFileData(getRClassWithReferenceToUnused())
+        .addKeepClassAndMembersRulesWithAllowObfuscation(
+            DescriptorUtils.descriptorToJavaType(RClassDescriptor))
+        .addAndroidResources(testResources)
+        .addKeepMainRule(FooBar.class)
+        .compile()
+        .inspectShrunkenResources(
+            resourceTableInspector -> {
+              resourceTableInspector.assertContainsResourceWithName("string", "bar");
+              resourceTableInspector.assertDoesNotContainResourceWithName(
+                  "string", "unused_string");
+            })
+        .run(parameters.getRuntime(), FooBar.class)
+        .assertSuccess();
+  }
+
+  public static class FooBar {
+
+    public static void main(String[] args) {
+      if (System.currentTimeMillis() == 0) {
+        System.out.println(R.string.bar);
+      }
+    }
+  }
+
+  // Simulate R class usage of unused_string
+  public static class ToBeRenamedToRDollarString {
+    public static int use_the_unused = RClassStaticValuesIgnoreTest.R.string.unused_string;
+  }
+
+  public static class R {
+    public static class string {
+
+      public static int bar;
+      public static int unused_string;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignaturePrunedInterfacesKeepTest.java b/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignaturePrunedInterfacesKeepTest.java
index 6d6b915..7cbbdf2 100644
--- a/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignaturePrunedInterfacesKeepTest.java
+++ b/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignaturePrunedInterfacesKeepTest.java
@@ -39,7 +39,8 @@
     testForR8(parameters.getBackend())
         .addInnerClasses(getClass())
         .setMinApi(parameters)
-        .addKeepAllClassesRule()
+        .addKeepMainRule(Main.class)
+        .addKeepClassRules(J.class, A.class, B.class)
         .addKeepAttributeSignature()
         .addKeepAttributeInnerClassesAndEnclosingMethod()
         .run(parameters.getRuntime(), Main.class)
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/UnusedButKeptInterfaceTest.java b/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/UnusedButKeptInterfaceTest.java
new file mode 100644
index 0000000..21f61ce
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/UnusedButKeptInterfaceTest.java
@@ -0,0 +1,77 @@
+// Copyright (c) 2024, 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.unusedinterfaces;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.StringUtils;
+import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+/* Regression test for b/318787479 */
+@RunWith(Parameterized.class)
+public class UnusedButKeptInterfaceTest extends TestBase {
+
+  static final String EXPECTED = StringUtils.lines("A: I", "B:", "C: I");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public UnusedButKeptInterfaceTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    testForRuntime(parameters)
+        .addInnerClasses(UnusedButKeptInterfaceTest.class)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(UnusedButKeptInterfaceTest.class)
+        .addKeepMainRule(TestClass.class)
+        .addKeepClassRules(I.class, A.class, B.class, C.class)
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  interface I {}
+
+  static class A implements I {}
+
+  static class B extends A {}
+
+  static class C extends A implements I {}
+
+  static class TestClass {
+
+    static String innerName(Class<?> clazz) {
+      String typeName = clazz.getName();
+      return typeName.substring(typeName.lastIndexOf('$') + 1);
+    }
+
+    public static void main(String[] args) {
+      for (Class<?> c : Arrays.asList(A.class, B.class, C.class)) {
+        System.out.print(innerName(c) + ":");
+        for (Class<?> iface : c.getInterfaces()) {
+          System.out.print(" " + innerName(iface));
+        }
+        System.out.println();
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/doctests/ConditionalMethodRulesAndHorizontalMergingTest.java b/src/test/java/com/android/tools/r8/keepanno/ConditionalMethodRulesAndHorizontalMergingTest.java
similarity index 88%
rename from src/test/java/com/android/tools/r8/keepanno/doctests/ConditionalMethodRulesAndHorizontalMergingTest.java
rename to src/test/java/com/android/tools/r8/keepanno/ConditionalMethodRulesAndHorizontalMergingTest.java
index ee1748d..8bf2e6a 100644
--- a/src/test/java/com/android/tools/r8/keepanno/doctests/ConditionalMethodRulesAndHorizontalMergingTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/ConditionalMethodRulesAndHorizontalMergingTest.java
@@ -1,15 +1,15 @@
-// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// Copyright (c) 2024, 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.doctests;
+package com.android.tools.r8.keepanno;
 
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.keepanno.doctests.ConditionalMethodRulesAndHorizontalMergingTest.Example1.BaseClass;
-import com.android.tools.r8.keepanno.doctests.ConditionalMethodRulesAndHorizontalMergingTest.Example1.MyHiddenMethodCaller;
-import com.android.tools.r8.keepanno.doctests.ConditionalMethodRulesAndHorizontalMergingTest.Example2.MyFieldValuePrinter;
-import com.android.tools.r8.keepanno.doctests.ConditionalMethodRulesAndHorizontalMergingTest.Example2.PrintableFieldInterface;
+import com.android.tools.r8.keepanno.ConditionalMethodRulesAndHorizontalMergingTest.Example1.BaseClass;
+import com.android.tools.r8.keepanno.ConditionalMethodRulesAndHorizontalMergingTest.Example1.MyHiddenMethodCaller;
+import com.android.tools.r8.keepanno.ConditionalMethodRulesAndHorizontalMergingTest.Example2.MyFieldValuePrinter;
+import com.android.tools.r8.keepanno.ConditionalMethodRulesAndHorizontalMergingTest.Example2.PrintableFieldInterface;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.StringUtils;
 import com.google.common.collect.ImmutableList;
diff --git a/src/test/java/com/android/tools/r8/keepanno/FieldNameStringPatternsTest.java b/src/test/java/com/android/tools/r8/keepanno/FieldNameStringPatternsTest.java
new file mode 100644
index 0000000..520c65c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/FieldNameStringPatternsTest.java
@@ -0,0 +1,117 @@
+// Copyright (c) 2024, 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;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.keepanno.annotations.KeepItemKind;
+import com.android.tools.r8.keepanno.annotations.KeepTarget;
+import com.android.tools.r8.keepanno.annotations.StringPattern;
+import com.android.tools.r8.keepanno.annotations.UsedByReflection;
+import com.android.tools.r8.keepanno.annotations.UsesReflection;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import java.lang.reflect.Field;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class FieldNameStringPatternsTest extends TestBase {
+
+  static final String EXPECTED = StringUtils.lines("1", "2");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
+  }
+
+  public FieldNameStringPatternsTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(getInputClasses())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .enableExperimentalKeepAnnotations()
+        .addProgramClasses(getInputClasses())
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(this::checkOutput);
+  }
+
+  public List<Class<?>> getInputClasses() {
+    return ImmutableList.of(TestClass.class, A.class, B.class);
+  }
+
+  private void checkOutput(CodeInspector inspector) {
+    assertThat(inspector.clazz(B.class), isPresentAndRenamed());
+    assertThat(inspector.clazz(B.class).field("int", "bar"), isAbsent());
+    assertThat(inspector.clazz(B.class).field("int", "hiddenOneI"), isPresent());
+    assertThat(inspector.clazz(B.class).field("int", "hiddenTwoI"), isPresent());
+    assertThat(inspector.clazz(B.class).field("java.lang.String", "hiddenSomeS"), isAbsent());
+  }
+
+  static class A {
+
+    @UsesReflection(
+        @KeepTarget(
+            classConstant = B.class,
+            fieldNamePattern = @StringPattern(startsWith = "hidden", endsWith = "I")))
+    public void foo() throws Exception {
+      int counter = 1;
+      for (Field field : B.class.getDeclaredFields()) {
+        String name = field.getName();
+        if (name.startsWith("hidden")) {
+          if (name.endsWith("I")) {
+            field.set(null, counter++);
+          }
+        }
+      }
+      for (Field field : B.class.getDeclaredFields()) {
+        String name = field.getName();
+        if (name.startsWith("hidden")) {
+          if (name.endsWith("I")) {
+            System.out.println(field.get(null));
+          }
+        }
+      }
+    }
+  }
+
+  static class B {
+    static int hiddenOneI = 9;
+    static int hiddenTwoI = 18;
+    static String hiddenSomeS = "foo";
+    static int bar = 7;
+  }
+
+  static class TestClass {
+
+    @UsedByReflection(kind = KeepItemKind.CLASS_AND_METHODS)
+    public static void main(String[] args) throws Exception {
+      new A().foo();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/ast/KeepEdgeAstTest.java b/src/test/java/com/android/tools/r8/keepanno/ast/KeepEdgeAstTest.java
index 8a83cf9..dae2c6b 100644
--- a/src/test/java/com/android/tools/r8/keepanno/ast/KeepEdgeAstTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/ast/KeepEdgeAstTest.java
@@ -250,7 +250,7 @@
 
   private KeepMemberPattern defaultInitializerPattern() {
     return KeepMethodPattern.builder()
-        .setNamePattern(KeepMethodNamePattern.initializer())
+        .setNamePattern(KeepMethodNamePattern.instanceInitializer())
         .setParametersPattern(KeepMethodParametersPattern.none())
         .setReturnTypeVoid()
         .build();
diff --git a/src/test/java/com/android/tools/r8/keepanno/doctests/ForApiDocumentationTest.java b/src/test/java/com/android/tools/r8/keepanno/doctests/ForApiDocumentationTest.java
new file mode 100644
index 0000000..7df53d6
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/doctests/ForApiDocumentationTest.java
@@ -0,0 +1,210 @@
+// 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.doctests;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.keepanno.annotations.KeepForApi;
+import com.android.tools.r8.keepanno.annotations.MemberAccessFlags;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableList;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class ForApiDocumentationTest extends TestBase {
+
+  static final String EXPECTED_1_REF =
+      StringUtils.joinLines(
+          "thisPackagePrivateMethodIsNotKept",
+          "thisPrivateMethodIsNotKept",
+          "thisProtectedMethodIsKept",
+          "thisPublicMethodIsKept");
+
+  static final String EXPECTED_1_R8 =
+      StringUtils.joinLines("thisProtectedMethodIsKept", "thisPublicMethodIsKept");
+
+  static final String EXPECTED_2_REF =
+      StringUtils.joinLines(
+          "thisPackagePrivateMethodIsKept",
+          "thisPrivateMethodIsNotKept",
+          "thisProtectedMethodIsKept",
+          "thisPublicMethodIsKept");
+
+  static final String EXPECTED_2_R8 =
+      StringUtils.joinLines(
+          "thisPackagePrivateMethodIsKept", "thisProtectedMethodIsKept", "thisPublicMethodIsKept");
+
+  static final String EXPECTED_3_REF = StringUtils.joinLines("isKept", "notKept");
+
+  static final String EXPECTED_3_R8 = StringUtils.joinLines("isKept");
+
+  static final String EXPECTED_REF =
+      StringUtils.lines(EXPECTED_1_REF, EXPECTED_2_REF, EXPECTED_3_REF);
+
+  static final String EXPECTED_R8 = StringUtils.lines(EXPECTED_1_R8, EXPECTED_2_R8, EXPECTED_3_R8);
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
+  }
+
+  public ForApiDocumentationTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(TestClass.class)
+        .addProgramClassesAndInnerClasses(getExampleClasses())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED_REF);
+  }
+
+  @Test
+  public void testWithRuleExtraction() throws Exception {
+    testForR8(parameters.getBackend())
+        .enableExperimentalKeepAnnotations()
+        .addProgramClasses(TestClass.class)
+        .addProgramClassesAndInnerClasses(getExampleClasses())
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED_R8);
+  }
+
+  public List<Class<?>> getExampleClasses() {
+    return ImmutableList.of(Example1.class, Example2.class, Example3.class);
+  }
+
+  static class Example1 {
+
+    /* INCLUDE DOC: ApiClass
+    When annotating a class the default for `@KeepForApi` is to keep the class as well as all of its
+    public and protected members:
+    INCLUDE END */
+
+    static
+    // INCLUDE CODE: ApiClass
+    @KeepForApi
+    public class MyApi {
+      public void thisPublicMethodIsKept() {
+        /* ... */
+      }
+
+      protected void thisProtectedMethodIsKept() {
+        /* ... */
+      }
+
+      void thisPackagePrivateMethodIsNotKept() {
+        /* ... */
+      }
+
+      private void thisPrivateMethodIsNotKept() {
+        /* ... */
+      }
+    }
+
+    // INCLUDE END
+
+    static void run() throws Exception {
+      TestClass.printMethods(MyApi.class);
+    }
+  }
+
+  static class Example2 {
+
+    /* INCLUDE DOC: ApiClassMemberAccess
+    The default can be changed using the `@KeepForApi#memberAccess` property:
+    INCLUDE END */
+
+    // INCLUDE CODE: ApiClassMemberAccess
+    @KeepForApi(
+        memberAccess = {
+          MemberAccessFlags.PUBLIC,
+          MemberAccessFlags.PROTECTED,
+          MemberAccessFlags.PACKAGE_PRIVATE
+        })
+    // INCLUDE END
+    public static class MyApi {
+      public void thisPublicMethodIsKept() {
+        /* ... */
+      }
+
+      protected void thisProtectedMethodIsKept() {
+        /* ... */
+      }
+
+      void thisPackagePrivateMethodIsKept() {
+        /* ... */
+      }
+
+      private void thisPrivateMethodIsNotKept() {
+        /* ... */
+      }
+    }
+
+    static void run() throws Exception {
+      TestClass.printMethods(MyApi.class);
+    }
+  }
+
+  static class Example3 {
+
+    /* INCLUDE DOC: ApiMember
+    The `@KeepForApi` annotation can also be placed directly on members and avoid keeping
+    unannotated members. The holder class is implicitly kept. When annotating the members
+    directly, the access does not matter as illustrated here by annotating a package private method:
+    INCLUDE END */
+
+    static
+    // INCLUDE CODE: ApiMember
+    public class MyOtherApi {
+
+      public void notKept() {
+        /* ... */
+      }
+
+      @KeepForApi
+      void isKept() {
+        /* ... */
+      }
+    }
+
+    // INCLUDE END
+
+    static void run() throws Exception {
+      TestClass.printMethods(MyOtherApi.class);
+    }
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) throws Exception {
+      Example1.run();
+      Example2.run();
+      Example3.run();
+    }
+
+    static void printMethods(Class<?> clazz) {
+      List<String> names = new ArrayList<>();
+      for (Method m : clazz.getDeclaredMethods()) {
+        names.add(m.getName());
+      }
+      names.sort(String::compareTo);
+      for (String name : names) {
+        System.out.println(name);
+      }
+    }
+  }
+}
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 0cf7639..98a2f0d 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
@@ -6,9 +6,11 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.keepanno.annotations.KeepCondition;
 import com.android.tools.r8.keepanno.annotations.KeepConstraint;
 import com.android.tools.r8.keepanno.annotations.KeepItemKind;
 import com.android.tools.r8.keepanno.annotations.KeepTarget;
+import com.android.tools.r8.keepanno.annotations.UsedByReflection;
 import com.android.tools.r8.keepanno.annotations.UsesReflection;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.StringUtils;
@@ -22,8 +24,17 @@
 @RunWith(Parameterized.class)
 public class UsesReflectionDocumentationTest extends TestBase {
 
+  static final String EXPECTED_METHOD_EXAMPLE = StringUtils.joinLines("on Base", "on Sub");
+  static final String EXPECTED_FIELD_EXAMPLE =
+      StringUtils.joinLines("intField = 42", "stringField = Hello!");
+
   static final String EXPECTED =
-      StringUtils.lines("on Base", "on Sub", "intField = 42", "stringField = Hello!");
+      StringUtils.lines(
+          EXPECTED_METHOD_EXAMPLE,
+          EXPECTED_FIELD_EXAMPLE,
+          EXPECTED_FIELD_EXAMPLE,
+          EXPECTED_FIELD_EXAMPLE,
+          EXPECTED_FIELD_EXAMPLE);
 
   private final TestParameters parameters;
 
@@ -58,7 +69,8 @@
   }
 
   public List<Class<?>> getExampleClasses() {
-    return ImmutableList.of(Example1.class, Example2.class);
+    return ImmutableList.of(
+        Example1.class, Example2.class, Example3.class, Example4.class, Example5.class);
   }
 
   static class Example1 {
@@ -160,11 +172,140 @@
     }
   }
 
+  static class Example3 {
+
+    interface PrintableFieldInterface {}
+
+    /* INCLUDE DOC: UsedByReflectionFieldPrinterOnFields
+    For example, the same field printing as in the above example might be part of a library.
+
+    In this example, the `MyClassWithFields` is a class you are passing to the
+    field-printing utility of the library. Since the library is reflectively accessing each field
+    we annotate them with the `@UsedByReflection` annotation.
+
+    We could additionally add the `@UsedByReflection#constraints` property as we did previously.
+    We elide it here for brevity.
+    INCLUDE END */
+
+    static
+    // INCLUDE CODE: UsedByReflectionFieldPrinterOnFields
+    public class MyClassWithFields implements PrintableFieldInterface {
+      @UsedByReflection final int intField = 42;
+
+      @UsedByReflection String stringField = "Hello!";
+    }
+
+    public static void run() throws Exception {
+      new FieldValuePrinterLibrary().printFieldValues(new MyClassWithFields());
+    }
+
+    // INCLUDE END
+
+    public static class FieldValuePrinterLibrary {
+
+      public void printFieldValues(PrintableFieldInterface objectWithFields) throws Exception {
+        for (Field field : objectWithFields.getClass().getDeclaredFields()) {
+          System.out.println(field.getName() + " = " + field.get(objectWithFields));
+        }
+      }
+    }
+  }
+
+  static class Example4 {
+
+    interface PrintableFieldInterface {}
+
+    /* INCLUDE DOC: UsedByReflectionFieldPrinterOnClass
+    Rather than annotate the individual fields we can annotate the holder and add a specification
+    similar to the `@KeepTarget`. The `@UsedByReflection#kind` specifies that only the fields are
+    used reflectively. In particular, the "field printer" example we are considering here does not
+    make reflective assumptions about the holder class, so we should not constrain it.
+
+    To be more precise let's add the `@UsedByReflection#constraints` property now. This specifies
+    that the fields are looked up, their names are used/assumed and their values are read.
+    INCLUDE END */
+
+    static
+    // INCLUDE CODE: UsedByReflectionFieldPrinterOnClass
+    @UsedByReflection(
+        kind = KeepItemKind.ONLY_FIELDS,
+        constraints = {KeepConstraint.LOOKUP, KeepConstraint.NAME, KeepConstraint.FIELD_GET})
+    public class MyClassWithFields implements PrintableFieldInterface {
+      final int intField = 42;
+      String stringField = "Hello!";
+    }
+
+    // INCLUDE END
+
+    public static void run() throws Exception {
+      new FieldValuePrinterLibrary().printFieldValues(new MyClassWithFields());
+    }
+
+    public static class FieldValuePrinterLibrary {
+
+      public void printFieldValues(PrintableFieldInterface objectWithFields) throws Exception {
+        for (Field field : objectWithFields.getClass().getDeclaredFields()) {
+          System.out.println(field.getName() + " = " + field.get(objectWithFields));
+        }
+      }
+    }
+  }
+
+  static class Example5 {
+
+    interface PrintableFieldInterface {}
+
+    /* INCLUDE DOC: UsedByReflectionFieldPrinterConditional
+    Our use of `@UsedByReflection` is still not as flexible as the original `@UsesReflection`. In
+    particular, if we change our code to no longer have any call to the library method
+    `printFieldValues` the shrinker will still keep all of the fields on our annotated class.
+
+    This is because the `@UsesReflection` implicitly encodes as a precondition that the annotated
+    method is actually used in the program. If not, the `@UsesReflection` annotation is not
+    "active".
+
+    Luckily we can specify the same precondition using `@UsedByReflection#preconditions`.
+    INCLUDE END */
+
+    static
+    // INCLUDE CODE: UsedByReflectionFieldPrinterConditional
+    @UsedByReflection(
+        preconditions = {
+          @KeepCondition(
+              classConstant = FieldValuePrinterLibrary.class,
+              methodName = "printFieldValues")
+        },
+        kind = KeepItemKind.ONLY_FIELDS,
+        constraints = {KeepConstraint.LOOKUP, KeepConstraint.NAME, KeepConstraint.FIELD_GET})
+    public class MyClassWithFields implements PrintableFieldInterface {
+      final int intField = 42;
+      String stringField = "Hello!";
+    }
+
+    // INCLUDE END
+
+    public static void run() throws Exception {
+      new FieldValuePrinterLibrary().printFieldValues(new MyClassWithFields());
+    }
+
+    public static class FieldValuePrinterLibrary {
+
+      public void printFieldValues(PrintableFieldInterface objectWithFields) throws Exception {
+        for (Field field : objectWithFields.getClass().getDeclaredFields()) {
+          System.out.println(field.getName() + " = " + field.get(objectWithFields));
+        }
+      }
+    }
+  }
+
   static class TestClass {
 
     public static void main(String[] args) throws Exception {
       Example1.run();
       Example2.run();
+      Example3.run();
+      Example4.run();
+      Example5.run();
     }
   }
 }
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
index 9088752..9376fa3 100644
--- a/src/test/java/com/android/tools/r8/keepanno/utils/KeepAnnoMarkdownGenerator.java
+++ b/src/test/java/com/android/tools/r8/keepanno/utils/KeepAnnoMarkdownGenerator.java
@@ -20,6 +20,7 @@
 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.ForApiDocumentationTest;
 import com.android.tools.r8.keepanno.doctests.MainMethodsDocumentationTest;
 import com.android.tools.r8.keepanno.doctests.UsesReflectionDocumentationTest;
 import com.android.tools.r8.keepanno.utils.KeepItemAnnotationGenerator.Generator;
@@ -92,7 +93,9 @@
             MethodAccessFlags.class,
             FieldAccessFlags.class);
     populateCodeAndDocReplacements(
-        UsesReflectionDocumentationTest.class, MainMethodsDocumentationTest.class);
+        UsesReflectionDocumentationTest.class,
+        ForApiDocumentationTest.class,
+        MainMethodsDocumentationTest.class);
   }
 
   private Map<String, String> getTypeLinkReplacements(Class<?>... classes) {
@@ -418,7 +421,7 @@
     }
 
     public String getIdAnchor() {
-      return name + "<a id=" + quote(id) + "></a>";
+      return name + "<a name=" + quote(id) + "></a>";
     }
   }
 }
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 0ab89fa..61265cb 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
@@ -821,7 +821,14 @@
                   .addParagraph(getMutuallyExclusiveForFieldProperties())
                   .addParagraph(getFieldDefaultDoc("any field name"))
                   .setDocReturn("The exact field name of the field.")
-                  .defaultEmptyString());
+                  .defaultEmptyString())
+          .addMember(
+              new GroupMember("fieldNamePattern")
+                  .setDocTitle("Define the field-name pattern by a string pattern.")
+                  .addParagraph(getMutuallyExclusiveForFieldProperties())
+                  .addParagraph(getFieldDefaultDoc("any field name"))
+                  .setDocReturn("The string pattern of the field name.")
+                  .defaultValue(StringPattern.class, DEFAULT_INVALID_STRING_PATTERN));
     }
 
     private Group createFieldTypeGroup() {
diff --git a/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java b/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java
index 12b507d..ba27997 100644
--- a/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java
@@ -257,6 +257,7 @@
                 testBuilder.addKeepRules(
                     "-neverinline class kotlin.jvm.internal.Intrinsics {",
                     "  *** checkNotNullParameter(...);",
+                    "  *** checkParameterIsNotNull(...);",
                     "}"))
         .inspect(
             inspector -> {
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayOfArraysTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayOfArraysTest.java
new file mode 100644
index 0000000..82ae38f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayOfArraysTest.java
@@ -0,0 +1,185 @@
+// Copyright (c) 2024, 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.rewrite.arrays;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ArrayOfArraysTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameter(1)
+  public CompilationMode compilationMode;
+
+  @Parameters(name = "{0}, mode = {1}")
+  public static Iterable<?> data() {
+    return buildParameters(
+        getTestParameters()
+            .withDefaultCfRuntime()
+            .withDexRuntimesAndAllApiLevels()
+            .withAllApiLevelsAlsoForCf()
+            .build(),
+        CompilationMode.values());
+  }
+
+  private static final String EXPECTED_OUTPUT =
+      StringUtils.lines(
+          "[class [B, class [S, class [I, class [J, class [C, class [F, class [D, class"
+              + " [Ljava.lang.String;, [class [B, class [S, class [I, class [J, class [C, class [F,"
+              + " class [D, class [Ljava.lang.String;]]",
+          "[class [B, class [S, class [I, class [J, class [C, class [F, class [D, class"
+              + " [Ljava.lang.String;, [class [B, class [S, class [I, class [J, class [C, class [F,"
+              + " class [D, class [Ljava.lang.String;]]");
+
+  private void inspect(MethodSubject method, int dexFilledNewArray, int cfMaxLocals) {
+    if (parameters.isDexRuntime()) {
+      assertEquals(
+          dexFilledNewArray,
+          method.streamInstructions().filter(InstructionSubject::isFilledNewArray).count());
+
+    } else {
+      assertEquals(
+          0, method.streamInstructions().filter(InstructionSubject::isFilledNewArray).count());
+      assertEquals(cfMaxLocals, method.getMethod().getCode().asCfCode().getMaxLocals());
+    }
+  }
+
+  private void inspect(CodeInspector inspector) {
+    inspect(
+        inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m1"),
+        2
+            + (canUseFilledNewArrayOfStringObjects(parameters) ? 2 : 0)
+            + (canUseFilledNewArrayOfNonStringObjects(parameters) ? 2 : 0),
+        2);
+    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m2"), 2, 2);
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    parameters.assumeDexRuntime();
+    testForD8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .setMinApi(parameters)
+        .setMode(compilationMode)
+        .run(parameters.getRuntime(), TestClass.class)
+        .inspect(this::inspect)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    parameters.assumeR8TestParameters();
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters)
+        .setMode(compilationMode)
+        .enableInliningAnnotations()
+        .addDontObfuscate()
+        .run(parameters.getRuntime(), TestClass.class)
+        .inspect(this::inspect)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+  }
+
+  public static final class TestClass {
+
+    @NeverInline
+    public static void m1() {
+      Object[] array = {
+        new byte[] {(byte) 1},
+        new short[] {(short) 1},
+        new int[] {1},
+        new long[] {1L},
+        new char[] {(char) 1},
+        new float[] {1.0f},
+        new double[] {1.0d},
+        new String[] {"one"},
+        new Object[] {
+          new byte[] {(byte) 2},
+          new short[] {(short) 2},
+          new int[] {2},
+          new long[] {2L},
+          new char[] {(char) 2},
+          new float[] {2.0f},
+          new double[] {2.0d},
+          new String[] {"two"},
+        }
+      };
+      printArray(array);
+      System.out.println();
+    }
+
+    @NeverInline
+    public static void m2() {
+      try {
+        Object[] array = {
+          new byte[] {(byte) 1},
+          new short[] {(short) 1},
+          new int[] {1},
+          new long[] {1L},
+          new char[] {(char) 1},
+          new float[] {1.0f},
+          new double[] {1.0d},
+          new String[] {"one"},
+          new Object[] {
+            new byte[] {(byte) 2},
+            new short[] {(short) 2},
+            new int[] {2},
+            new long[] {2L},
+            new char[] {(char) 2},
+            new float[] {2.0f},
+            new double[] {2.0d},
+            new String[] {"two"},
+          }
+        };
+        printArray(array);
+        System.out.println();
+      } catch (Exception e) {
+        throw new RuntimeException();
+      }
+    }
+
+    @NeverInline
+    public static void printArray(Object[] array) {
+      System.out.print("[");
+      for (int i = 0; i < array.length; i++) {
+        if (i != 0) {
+          System.out.print(", ");
+        }
+        if (array[i].getClass().isArray()) {
+          if (array[i] instanceof Object[] && !(array[i] instanceof String[])) {
+            printArray((Object[]) array[i]);
+          } else {
+            System.out.print(array[i].getClass());
+          }
+        } else {
+          System.out.print("Unexpected " + array[i].getClass());
+        }
+      }
+      System.out.print("]");
+    }
+
+    public static void main(String[] args) {
+      m1();
+      m2();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayOfConstClassArraysTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayOfConstClassArraysTest.java
new file mode 100644
index 0000000..63fec32
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayOfConstClassArraysTest.java
@@ -0,0 +1,179 @@
+// Copyright (c) 2024, 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.rewrite.arrays;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.rewrite.arrays.ConstClassArrayWithUniqueValuesTest.TestClass;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ArrayOfConstClassArraysTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameter(1)
+  public CompilationMode compilationMode;
+
+  @Parameters(name = "{0}, mode = {1}")
+  public static Iterable<?> data() {
+    return buildParameters(
+        getTestParameters()
+            .withDefaultCfRuntime()
+            .withDexRuntimesAndAllApiLevels()
+            .withAllApiLevelsAlsoForCf()
+            .build(),
+        CompilationMode.values());
+  }
+
+  private static final String EXPECTED_OUTPUT =
+      StringUtils.lines(
+          "[[A, B], [C, D], [E, F, G], [H, I, J]]", "[[J, I, H], [G, F, E], [D, C], [B, A]]");
+
+  private void inspect(MethodSubject method, int dexFilledNewArray, int cfMaxLocals) {
+    if (canUseFilledNewArrayOfNonStringObjects(parameters)) {
+      assertEquals(
+          dexFilledNewArray,
+          method.streamInstructions().filter(InstructionSubject::isFilledNewArray).count());
+
+    } else {
+      assertEquals(
+          0, method.streamInstructions().filter(InstructionSubject::isFilledNewArray).count());
+      if (parameters.isCfRuntime()) {
+        assertEquals(cfMaxLocals, method.getMethod().getCode().asCfCode().getMaxLocals());
+      }
+    }
+  }
+
+  private void inspect(CodeInspector inspector) {
+    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m1"), 5, 1);
+    inspect(
+        inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m2"),
+        0,
+        compilationMode.isDebug() ? 1 : 2);
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    parameters.assumeDexRuntime();
+    testForD8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .setMinApi(parameters)
+        .setMode(compilationMode)
+        .run(parameters.getRuntime(), TestClass.class)
+        .inspect(this::inspect)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    parameters.assumeR8TestParameters();
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters)
+        .setMode(compilationMode)
+        .enableInliningAnnotations()
+        .addDontObfuscate()
+        .run(parameters.getRuntime(), TestClass.class)
+        .inspect(this::inspect)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+  }
+
+  public static final class TestClass {
+
+    @NeverInline
+    public static void m1() {
+      Class<?>[][] array = {
+        new Class<?>[] {A.class, B.class},
+        new Class<?>[] {C.class, D.class},
+        new Class<?>[] {E.class, F.class, G.class},
+        new Class<?>[] {H.class, I.class, J.class}
+      };
+      printArray(array);
+    }
+
+    @NeverInline
+    public static void m2() {
+      try {
+        Class<?>[][] array = {
+          new Class<?>[] {J.class, I.class, H.class},
+          new Class<?>[] {G.class, F.class, E.class},
+          new Class<?>[] {D.class, C.class},
+          new Class<?>[] {B.class, A.class}
+        };
+        printArray(array);
+      } catch (Exception e) {
+        throw new RuntimeException();
+      }
+    }
+
+    @NeverInline
+    public static void printArray(Class<?>[][] array) {
+      System.out.print("[");
+      for (int i = 0; i < array.length; i++) {
+        if (i != 0) {
+          System.out.print(", ");
+        }
+        printArray(array[i]);
+      }
+      System.out.println("]");
+    }
+
+    @NeverInline
+    public static void printArray(Class<?>[] array) {
+      System.out.print("[");
+      for (int i = 0; i < array.length; i++) {
+        if (i != 0) {
+          System.out.print(", ");
+        }
+        String simpleName = array[i].getName();
+        if (simpleName.lastIndexOf("$") > 0) {
+          simpleName = simpleName.substring(simpleName.lastIndexOf("$") + 1);
+        }
+        System.out.print(simpleName);
+      }
+      System.out.print("]");
+    }
+
+    public static void main(String[] args) {
+      m1();
+      m2();
+    }
+  }
+
+  class A {}
+
+  class B {}
+
+  class C {}
+
+  class D {}
+
+  class E {}
+
+  class F {}
+
+  class G {}
+
+  class H {}
+
+  class I {}
+
+  class J {}
+}
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayOfIntArraysTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayOfIntArraysTest.java
new file mode 100644
index 0000000..edc54de
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayOfIntArraysTest.java
@@ -0,0 +1,151 @@
+// Copyright (c) 2024, 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.rewrite.arrays;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ArrayOfIntArraysTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameter(1)
+  public CompilationMode compilationMode;
+
+  @Parameters(name = "{0}, mode = {1}")
+  public static Iterable<?> data() {
+    return buildParameters(
+        getTestParameters()
+            .withDefaultCfRuntime()
+            .withDexRuntimesAndAllApiLevels()
+            .withAllApiLevelsAlsoForCf()
+            .build(),
+        CompilationMode.values());
+  }
+
+  private static final String EXPECTED_OUTPUT =
+      StringUtils.lines(
+          "[[1, 2], [3, 4], [5, 6, 7], [8, 9, 10]]", "[[10, 9, 8], [7, 6, 5], [4, 3], [2, 1]]");
+
+  private void inspect(MethodSubject method, int dexFilledNewArray, int cfMaxLocals) {
+    if (parameters.isDexRuntime()) {
+      assertEquals(
+          dexFilledNewArray,
+          method.streamInstructions().filter(InstructionSubject::isFilledNewArray).count());
+
+    } else {
+      assertEquals(
+          0, method.streamInstructions().filter(InstructionSubject::isFilledNewArray).count());
+      assertEquals(cfMaxLocals, method.getMethod().getCode().asCfCode().getMaxLocals());
+    }
+  }
+
+  private void inspect(CodeInspector inspector) {
+    // This test use smaller int arrays, where filled-new-array is preferred over filled-array-data.
+    inspect(
+        inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m1"),
+        4 + (canUseFilledNewArrayOfNonStringObjects(parameters) ? 1 : 0),
+        1);
+    // With catch handler the int[][] creation is not converted to filled-new-array.
+    inspect(
+        inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m2"),
+        4,
+        compilationMode.isDebug() ? 1 : 2);
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    parameters.assumeDexRuntime();
+    testForD8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .setMinApi(parameters)
+        .setMode(compilationMode)
+        .run(parameters.getRuntime(), TestClass.class)
+        .inspect(this::inspect)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    parameters.assumeR8TestParameters();
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters)
+        .setMode(compilationMode)
+        .enableInliningAnnotations()
+        .addDontObfuscate()
+        .run(parameters.getRuntime(), TestClass.class)
+        .inspect(this::inspect)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+  }
+
+  public static final class TestClass {
+
+    @NeverInline
+    public static void m1() {
+      int[][] array = {
+        new int[] {1, 2}, new int[] {3, 4}, new int[] {5, 6, 7}, new int[] {8, 9, 10}
+      };
+      printArray(array);
+    }
+
+    @NeverInline
+    public static void m2() {
+      try {
+        int[][] array = {
+          new int[] {10, 9, 8}, new int[] {7, 6, 5}, new int[] {4, 3}, new int[] {2, 1}
+        };
+        printArray(array);
+      } catch (Exception e) {
+        throw new RuntimeException();
+      }
+    }
+
+    @NeverInline
+    public static void printArray(int[][] array) {
+      System.out.print("[");
+      for (int i = 0; i < array.length; i++) {
+        if (i != 0) {
+          System.out.print(", ");
+        }
+        printArray(array[i]);
+      }
+      System.out.println("]");
+    }
+
+    @NeverInline
+    public static void printArray(int[] array) {
+      System.out.print("[");
+      for (int i = 0; i < array.length; i++) {
+        if (i != 0) {
+          System.out.print(", ");
+        }
+        System.out.print(array[i]);
+      }
+      System.out.print("]");
+    }
+
+    public static void main(String[] args) {
+      m1();
+      m2();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayOfStringArraysTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayOfStringArraysTest.java
new file mode 100644
index 0000000..1934f24
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayOfStringArraysTest.java
@@ -0,0 +1,235 @@
+// Copyright (c) 2024, 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.rewrite.arrays;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.rewrite.arrays.ArrayOfConstClassArraysTest.TestClass;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ArrayOfStringArraysTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameter(1)
+  public CompilationMode compilationMode;
+
+  @Parameters(name = "{0}, mode = {1}")
+  public static Iterable<?> data() {
+    return buildParameters(
+        getTestParameters()
+            .withDefaultCfRuntime()
+            .withDexRuntimesAndAllApiLevels()
+            .withAllApiLevelsAlsoForCf()
+            .build(),
+        CompilationMode.values());
+  }
+
+  private static final String EXPECTED_OUTPUT =
+      StringUtils.lines(
+          "[[A, B], [C, D], [E, F, G], [H, I, J]]",
+          "[[J, I, H], [G, F, E], [D, C], [B, A]]",
+          "[[[A, B, C]], [[D, E, F]], [[G, H]], [[I, J]]]",
+          "[[[J, I, H]], [[G, F, E]], [[D, C]], [[B, A]]]",
+          "[[J, I, H], [G, F, E], [D, C], [B, A]]");
+
+  private void inspect(MethodSubject method, int dexFilledNewArray, int cfMaxLocals) {
+    if (parameters.isDexRuntime()) {
+      assertEquals(
+          dexFilledNewArray,
+          method.streamInstructions().filter(InstructionSubject::isFilledNewArray).count());
+
+    } else {
+      assertEquals(
+          0, method.streamInstructions().filter(InstructionSubject::isFilledNewArray).count());
+      assertEquals(cfMaxLocals, method.getMethod().getCode().asCfCode().getMaxLocals());
+    }
+  }
+
+  private void inspect(CodeInspector inspector) {
+    inspect(
+        inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m1"),
+        (canUseFilledNewArrayOfStringObjects(parameters) ? 4 : 0)
+            + (canUseFilledNewArrayOfNonStringObjects(parameters) ? 1 : 0),
+        1);
+    inspect(
+        inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m2"),
+        0,
+        compilationMode.isDebug() ? 2 : 1);
+    inspect(
+        inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m3"),
+        (canUseFilledNewArrayOfStringObjects(parameters) ? 4 : 0)
+            + (canUseFilledNewArrayOfNonStringObjects(parameters) ? 5 : 0),
+        5);
+    inspect(
+        inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m4"),
+        0,
+        compilationMode.isDebug() ? 2 : 1);
+    inspect(
+        inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m5"),
+        (canUseFilledNewArrayOfStringObjects(parameters) ? 4 : 0)
+            + (canUseFilledNewArrayOfNonStringObjects(parameters) ? 1 : 0),
+        compilationMode.isDebug() ? 6 : 4);
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    parameters.assumeDexRuntime();
+    testForD8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .setMinApi(parameters)
+        .setMode(compilationMode)
+        .run(parameters.getRuntime(), TestClass.class)
+        .inspect(this::inspect)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    parameters.assumeR8TestParameters();
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters)
+        .setMode(compilationMode)
+        .enableInliningAnnotations()
+        .addDontObfuscate()
+        .run(parameters.getRuntime(), TestClass.class)
+        .inspect(this::inspect)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+  }
+
+  public static final class TestClass {
+
+    @NeverInline
+    public static void m1() {
+      String[][] array = {
+        new String[] {"A", "B"},
+        new String[] {"C", "D"},
+        new String[] {"E", "F", "G"},
+        new String[] {"H", "I", "J"}
+      };
+      printArray(array);
+      System.out.println();
+    }
+
+    @NeverInline
+    public static void m2() {
+      try {
+        String[][] array = {
+          new String[] {"J", "I", "H"},
+          new String[] {"G", "F", "E"},
+          new String[] {"D", "C"},
+          new String[] {"B", "A"}
+        };
+        printArray(array);
+        System.out.println();
+      } catch (Exception e) {
+        throw new RuntimeException();
+      }
+    }
+
+    @NeverInline
+    public static void m3() {
+      String[][][] array = {
+        new String[][] {new String[] {"A", "B", "C"}},
+        new String[][] {new String[] {"D", "E", "F"}},
+        new String[][] {new String[] {"G", "H"}},
+        new String[][] {new String[] {"I", "J"}}
+      };
+      printArray(array);
+      System.out.println();
+    }
+
+    @NeverInline
+    public static void m4() {
+      try {
+        String[][][] array = {
+          new String[][] {new String[] {"J", "I", "H"}},
+          new String[][] {new String[] {"G", "F", "E"}},
+          new String[][] {new String[] {"D", "C"}},
+          new String[][] {new String[] {"B", "A"}}
+        };
+        printArray(array);
+        System.out.println();
+      } catch (Exception e) {
+        throw new RuntimeException();
+      }
+    }
+
+    @NeverInline
+    public static void m5() {
+      String[] a1 = new String[] {"J", "I", "H"};
+      String[] a2 = new String[] {"G", "F", "E"};
+      String[] a3 = new String[] {"D", "C"};
+      String[] a4 = new String[] {"B", "A"};
+      try {
+        String[][] array = {a1, a2, a3, a4};
+        printArray(array);
+        System.out.println();
+      } catch (Exception e) {
+        throw new RuntimeException();
+      }
+    }
+
+    @NeverInline
+    public static void printArray(String[][][] array) {
+      System.out.print("[");
+      for (int i = 0; i < array.length; i++) {
+        if (i != 0) {
+          System.out.print(", ");
+        }
+        printArray(array[i]);
+      }
+      System.out.print("]");
+    }
+
+    @NeverInline
+    public static void printArray(String[][] array) {
+      System.out.print("[");
+      for (int i = 0; i < array.length; i++) {
+        if (i != 0) {
+          System.out.print(", ");
+        }
+        printArray(array[i]);
+      }
+      System.out.print("]");
+    }
+
+    @NeverInline
+    public static void printArray(String[] array) {
+      System.out.print("[");
+      for (int i = 0; i < array.length; i++) {
+        if (i != 0) {
+          System.out.print(", ");
+        }
+        System.out.print(array[i]);
+      }
+      System.out.print("]");
+    }
+
+    public static void main(String[] args) {
+      m1();
+      m2();
+      m3();
+      m4();
+      m5();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/ConstClassArrayTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/ConstClassArrayTest.java
index 180c412..4b6e64c 100644
--- a/src/test/java/com/android/tools/r8/rewrite/arrays/ConstClassArrayTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/ConstClassArrayTest.java
@@ -41,11 +41,6 @@
   private static final String EXPECTED_OUTPUT =
       StringUtils.lines("[A, B, C, D, E]", "[E, D, C, B, A]");
 
-  public boolean canUseFilledNewArrayOfNonStringObjects(TestParameters parameters) {
-    return parameters.isDexRuntime()
-        && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.N);
-  }
-
   private enum State {
     EXPECTING_CONSTCLASS,
     EXPECTING_APUTOBJECT
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/ConstClassArrayWithNonUniqueValuesTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/ConstClassArrayWithNonUniqueValuesTest.java
index 213e3e4..3a364dd 100644
--- a/src/test/java/com/android/tools/r8/rewrite/arrays/ConstClassArrayWithNonUniqueValuesTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/ConstClassArrayWithNonUniqueValuesTest.java
@@ -12,7 +12,6 @@
 import com.android.tools.r8.TestCompilerBuilder;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.dex.Constants;
-import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.InstructionSubject;
@@ -47,15 +46,10 @@
 
   private static final String EXPECTED_OUTPUT = StringUtils.lines("100", "104");
 
-  public boolean canUseFilledNewArrayOfClass(TestParameters parameters) {
-    return parameters.isDexRuntime()
-        && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.N);
-  }
-
   private void inspect(
       MethodSubject method, int constClasses, int puts, boolean insideCatchHandler) {
     boolean expectingFilledNewArray =
-        canUseFilledNewArrayOfClass(parameters) && !insideCatchHandler;
+        canUseFilledNewArrayOfNonStringObjects(parameters) && !insideCatchHandler;
     assertEquals(
         expectingFilledNewArray ? 0 : puts,
         method.streamInstructions().filter(InstructionSubject::isArrayPut).count());
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/ConstClassArrayWithUniqueValuesTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/ConstClassArrayWithUniqueValuesTest.java
index d8227dc..c7f847b 100644
--- a/src/test/java/com/android/tools/r8/rewrite/arrays/ConstClassArrayWithUniqueValuesTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/ConstClassArrayWithUniqueValuesTest.java
@@ -11,7 +11,6 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.dex.Constants;
-import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.InstructionSubject;
@@ -48,11 +47,6 @@
   private static final String EXPECTED_OUTPUT =
       StringUtils.lines("[A00, A01, A02, A03, A04]", "[A00, A01, A02, A03, A04]", "100");
 
-  public boolean canUseFilledNewArrayOfClass(TestParameters parameters) {
-    return parameters.isDexRuntime()
-        && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.N);
-  }
-
   private enum State {
     EXPECTING_CONSTCLASS,
     EXPECTING_APUTOBJECT
@@ -60,7 +54,7 @@
 
   private void inspect(MethodSubject method, int puts, boolean insideCatchHandler) {
     boolean expectingFilledNewArray =
-        canUseFilledNewArrayOfClass(parameters) && !insideCatchHandler;
+        canUseFilledNewArrayOfNonStringObjects(parameters) && !insideCatchHandler;
     assertEquals(
         expectingFilledNewArray ? 0 : puts,
         method.streamInstructions().filter(InstructionSubject::isArrayPut).count());
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/IntegerArrayTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/IntegerArrayTest.java
index 54abbcd..1c159da 100644
--- a/src/test/java/com/android/tools/r8/rewrite/arrays/IntegerArrayTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/IntegerArrayTest.java
@@ -41,10 +41,6 @@
       StringUtils.lines(
           "[-2147483648, -1, 0, 1, 2147483647]", "[-2147483647, -2, 0, 2, 2147483646]");
 
-  public boolean canUseFilledNewArrayOfInteger(TestParameters parameters) {
-    return parameters.isDexRuntime();
-  }
-
   private void inspect(MethodSubject main) {
     assertEquals(
         canUseFilledNewArrayOfInteger(parameters) ? 0 : 5,
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/SimplifyArrayConstructionTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/SimplifyArrayConstructionTest.java
index 3f75ac1..c80436c 100644
--- a/src/test/java/com/android/tools/r8/rewrite/arrays/SimplifyArrayConstructionTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/SimplifyArrayConstructionTest.java
@@ -212,14 +212,12 @@
       assertArrayTypes(reversedArray, DexFilledNewArray.class);
     }
 
-    // Cannot use filled-new-array of String before K.
-    if (parameters.getApiLevel().isLessThan(AndroidApiLevel.K)) {
+    if (!canUseFilledNewArrayOfStringObjects(parameters)) {
       assertArrayTypes(stringArrays, DexNewArray.class);
     } else {
       assertArrayTypes(stringArrays, DexFilledNewArray.class);
     }
-    // Cannot use filled-new-array of Object before L.
-    if (parameters.getApiLevel().isLessThan(AndroidApiLevel.N)) {
+    if (!canUseFilledNewArrayOfNonStringObjects(parameters)) {
       assertArrayTypes(referenceArraysNoCasts, DexNewArray.class);
       assertArrayTypes(referenceArraysWithSubclasses, DexNewArray.class);
       assertArrayTypes(referenceArraysWithInterfaceImplementations, DexNewArray.class);
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/StaticGetArrayWithNonUniqueValuesTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/StaticGetArrayWithNonUniqueValuesTest.java
index 8623e55..be54d34 100644
--- a/src/test/java/com/android/tools/r8/rewrite/arrays/StaticGetArrayWithNonUniqueValuesTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/StaticGetArrayWithNonUniqueValuesTest.java
@@ -12,7 +12,6 @@
 import com.android.tools.r8.TestCompilerBuilder;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.dex.Constants;
-import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.InstructionSubject;
@@ -47,14 +46,9 @@
 
   private static final String EXPECTED_OUTPUT = StringUtils.lines("100", "50");
 
-  public boolean canUseFilledNewArrayOfObject(TestParameters parameters) {
-    return parameters.isDexRuntime()
-        && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.N);
-  }
-
   private void inspect(MethodSubject method, int staticGets, int puts, boolean insideCatchHandler) {
     boolean expectingFilledNewArray =
-        canUseFilledNewArrayOfObject(parameters) && !insideCatchHandler;
+        canUseFilledNewArrayOfNonStringObjects(parameters) && !insideCatchHandler;
     assertEquals(
         expectingFilledNewArray ? 0 : puts,
         method.streamInstructions().filter(InstructionSubject::isArrayPut).count());
@@ -74,12 +68,14 @@
   private void inspectD8(CodeInspector inspector) {
     inspect(
         inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m1"),
-        canUseFilledNewArrayOfObject(parameters) ? 100 : 1,
+        canUseFilledNewArrayOfNonStringObjects(parameters) ? 100 : 1,
         100,
         false);
     inspect(
         inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m2"),
-        canUseFilledNewArrayOfObject(parameters) ? 50 : (maxMaterializingConstants == 2 ? 42 : 10),
+        canUseFilledNewArrayOfNonStringObjects(parameters)
+            ? 50
+            : (maxMaterializingConstants == 2 ? 42 : 10),
         50,
         false);
   }
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/StaticGetArrayWithUniqueValuesTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/StaticGetArrayWithUniqueValuesTest.java
index b5fe270..fce1e79 100644
--- a/src/test/java/com/android/tools/r8/rewrite/arrays/StaticGetArrayWithUniqueValuesTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/StaticGetArrayWithUniqueValuesTest.java
@@ -11,7 +11,6 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.dex.Constants;
-import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.InstructionSubject;
@@ -48,11 +47,6 @@
   private static final String EXPECTED_OUTPUT =
       StringUtils.lines("[A00, A01, A02, A03, A04]", "[A00, A01, A02, A03, A04]", "100");
 
-  public boolean canUseFilledNewArrayOfObject(TestParameters parameters) {
-    return parameters.isDexRuntime()
-        && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.N);
-  }
-
   private enum State {
     EXPECTING_GETSTATIC,
     EXPECTING_APUTOBJECT
@@ -60,7 +54,7 @@
 
   private void inspect(MethodSubject method, int puts, boolean insideCatchHandler) {
     boolean expectingFilledNewArray =
-        canUseFilledNewArrayOfObject(parameters) && !insideCatchHandler;
+        canUseFilledNewArrayOfNonStringObjects(parameters) && !insideCatchHandler;
     assertEquals(
         expectingFilledNewArray ? 0 : puts,
         method.streamInstructions().filter(InstructionSubject::isArrayPut).count());
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/StringArrayWithNonUniqueValuesTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/StringArrayWithNonUniqueValuesTest.java
index 9100e39..c433e6d 100644
--- a/src/test/java/com/android/tools/r8/rewrite/arrays/StringArrayWithNonUniqueValuesTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/StringArrayWithNonUniqueValuesTest.java
@@ -12,7 +12,6 @@
 import com.android.tools.r8.TestCompilerBuilder;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.dex.Constants;
-import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.InstructionSubject;
@@ -47,15 +46,10 @@
 
   private static final String EXPECTED_OUTPUT = StringUtils.lines("100", "104");
 
-  public boolean canUseFilledNewArrayOfStrings(TestParameters parameters) {
-    return parameters.isDexRuntime()
-        && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.K);
-  }
-
   private void inspect(
       MethodSubject method, int constStrings, int puts, boolean insideCatchHandler) {
     boolean expectingFilledNewArray =
-        canUseFilledNewArrayOfStrings(parameters) && !insideCatchHandler;
+        canUseFilledNewArrayOfStringObjects(parameters) && !insideCatchHandler;
     assertEquals(
         expectingFilledNewArray ? 0 : puts,
         method.streamInstructions().filter(InstructionSubject::isArrayPut).count());
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/StringArrayWithUniqueValuesTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/StringArrayWithUniqueValuesTest.java
index 2613ca5..837bea2 100644
--- a/src/test/java/com/android/tools/r8/rewrite/arrays/StringArrayWithUniqueValuesTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/StringArrayWithUniqueValuesTest.java
@@ -11,7 +11,6 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.dex.Constants;
-import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.InstructionSubject;
@@ -48,11 +47,6 @@
   private static final String EXPECTED_OUTPUT =
       StringUtils.lines("[A, B, C, D, E]", "[F, G, H, I, J]", "100");
 
-  public boolean canUseFilledNewArrayOfStrings(TestParameters parameters) {
-    return parameters.isDexRuntime()
-        && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.K);
-  }
-
   private enum State {
     EXPECTING_CONSTSTRING,
     EXPECTING_APUTOBJECT
@@ -60,7 +54,7 @@
 
   private void inspect(MethodSubject method, int puts, boolean insideCatchHandler) {
     boolean expectingFilledNewArray =
-        canUseFilledNewArrayOfStrings(parameters) && !insideCatchHandler;
+        canUseFilledNewArrayOfStringObjects(parameters) && !insideCatchHandler;
     assertEquals(
         expectingFilledNewArray ? 0 : puts,
         method.streamInstructions().filter(InstructionSubject::isArrayPut).count());
diff --git a/tools/archive.py b/tools/archive.py
index a906a82..d9f8335 100755
--- a/tools/archive.py
+++ b/tools/archive.py
@@ -122,6 +122,20 @@
     print('INFO: Open files hard limit: %s' % hard)
 
 
+def RSyncDir(src_dir, version_or_path, dst_dir, is_main, options):
+    destination = GetUploadDestination(version_or_path, dst_dir, is_main)
+    print(f'RSyncing {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 syncing. Copying to {dry_run_destination}')
+            shutil.copytree(src_dir, dry_run_destination)
+        else:
+            print('Dry run, not actually uploading')
+    else:
+        utils.rsync_directory_to_cloud_storage(src_dir, destination)
+        print(f'Directory available at: {GetUrl(version_or_path, dst_dir, is_main)}')
+
 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}')
@@ -250,7 +264,7 @@
         if is_main:
             version_or_path = 'docs'
             dst_dir = 'keepanno/javadoc'
-            UploadDir(utils.KEEPANNO_ANNOTATIONS_DOC, version_or_path, dst_dir, is_main, options)
+            RSyncDir(utils.KEEPANNO_ANNOTATIONS_DOC, version_or_path, dst_dir, is_main, options)
 
         # Upload directories.
         dirs_for_archiving = [
diff --git a/tools/utils.py b/tools/utils.py
index e859748..1df6d83 100644
--- a/tools/utils.py
+++ b/tools/utils.py
@@ -393,11 +393,36 @@
     PrintCmd(cmd)
     subprocess.check_call(cmd)
 
+def check_dir_args(source, destination):
+    # We require that the dirname of the paths coincide, e.g., src/dirname and dst/dirname
+    # The target is then stripped so the upload command will be: cp -R src/dirname dst/
+    (destination_parent, destination_file) = os.path.split(destination)
+    if os.path.basename(source) != destination_file:
+        raise Exception(
+            'Attempt to upload directory with non-matching directory name: ' +
+            f'{source} and {destination}')
+    if len(destination_parent.strip()) == 0:
+        raise Exception(
+            'Attempt to upload directory to empty destination directory: '
+            + destination)
+    return destination_parent
+
 def upload_directory_to_cloud_storage(source, destination, parallel=True):
+    destination_parent = check_dir_args(source, destination)
     cmd = [get_gsutil()]
     if parallel:
         cmd += ['-m']
     cmd += ['cp', '-R']
+    cmd += [source, destination_parent + '/']
+    PrintCmd(cmd)
+    subprocess.check_call(cmd)
+
+def rsync_directory_to_cloud_storage(source, destination, parallel=True):
+    check_dir_args(source, destination)
+    cmd = [get_gsutil()]
+    if parallel:
+        cmd += ['-m']
+    cmd += ['rsync', '-R']
     cmd += [source, destination]
     PrintCmd(cmd)
     subprocess.check_call(cmd)