Allow generation of resources based on test R classes

This allows you to write R classes directly in tests and access the fields from other classes.

The R classes are used to populate the resources with default values
(the name for strings, a small png for drawables)

The non R classes are rewritten to point at the right R class fields.
We need this to avoid having the outer class.

Bug: b/287398085
Change-Id: I3ecc883f4c6f4ebbc91aceb6b0db65e044a3dd2d
diff --git a/src/test/java/com/android/tools/r8/TestBuilder.java b/src/test/java/com/android/tools/r8/TestBuilder.java
index 1105fac..4e98e36 100644
--- a/src/test/java/com/android/tools/r8/TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestBuilder.java
@@ -5,6 +5,7 @@
 
 import com.android.tools.r8.ClassFileConsumer.ArchiveConsumer;
 import com.android.tools.r8.TestBase.Backend;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResource;
 import com.android.tools.r8.debug.DebugTestConfig;
 import com.android.tools.r8.errors.Unimplemented;
 import com.android.tools.r8.utils.AndroidApiLevel;
@@ -135,6 +136,10 @@
     return addProgramClasses(classes).addInnerClasses(classes);
   }
 
+  public T addAndroidResources(AndroidTestResource testResource) throws IOException {
+    return addProgramClassFileData(testResource.getRClass().getClassFileData());
+  }
+
   public T addInnerClasses(Class<?>... classes) throws IOException {
     return addInnerClasses(Arrays.asList(classes));
   }
diff --git a/src/test/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java b/src/test/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java
index bf9ee64..6130b2a 100644
--- a/src/test/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java
+++ b/src/test/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java
@@ -3,22 +3,60 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.androidresources;
 
+import static com.android.tools.r8.TestBase.javac;
+import static com.android.tools.r8.TestBase.transformer;
+
+import com.android.tools.r8.TestRuntime.CfRuntime;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.transformers.ClassTransformer;
 import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.StreamUtils;
+import com.android.tools.r8.utils.ZipUtils;
 import com.google.common.collect.MoreCollectors;
 import java.io.IOException;
+import java.lang.reflect.Field;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.TreeMap;
+import java.util.stream.Collectors;
 import org.junit.rules.TemporaryFolder;
 
 public class AndroidResourceTestingUtils {
 
-  public static String SIMPLE_MANIFEST_WITH_STRING =
+  enum RClassType {
+    STRING,
+    DRAWABLE;
+
+    public static RClassType fromClass(Class clazz) {
+      String type = rClassWithoutNamespaceAndOuter(clazz).substring(2);
+      return RClassType.valueOf(type.toUpperCase());
+    }
+  }
+
+  private static String rClassWithoutNamespaceAndOuter(Class clazz) {
+    return rClassWithoutNamespaceAndOuter(clazz.getName());
+  }
+
+  private static String rClassWithoutNamespaceAndOuter(String name) {
+    assert isInnerRClass(name);
+    int dollarIndex = name.lastIndexOf("$");
+    String specificRClass = name.substring(dollarIndex - 1);
+    return specificRClass;
+  }
+
+  private static boolean isInnerRClass(String name) {
+    int dollarIndex = name.lastIndexOf("$");
+    return dollarIndex > 0 && name.charAt(dollarIndex - 1) == 'R';
+  }
+
+  public static String SIMPLE_MANIFEST_WITH_APP_NAME =
       "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
           + "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n"
           + "          package=\"com.android.tools.r8\">\n"
@@ -28,23 +66,22 @@
           + "\n";
 
   public static class AndroidTestRClass {
+    // The original aapt2 generated R.java class
     private final Path javaFilePath;
-    private final Path rootDirectory;
+    // The compiled class files, with the class names rewritten to the names used in passed
+    // in R class from the test.
+    private final List<byte[]> classFileData;
 
-    AndroidTestRClass(Path rootDirectory) throws IOException {
-      this.rootDirectory = rootDirectory;
-      this.javaFilePath =
-          Files.walk(rootDirectory)
-              .filter(path -> path.endsWith("R.java"))
-              .collect(MoreCollectors.onlyElement());
+    AndroidTestRClass(Path javaFilePath, List<byte[]> classFileData) throws IOException {
+      this.javaFilePath = javaFilePath;
+      this.classFileData = classFileData;
     }
-
     public Path getJavaFilePath() {
       return javaFilePath;
     }
 
-    public Path getRootDirectory() {
-      return rootDirectory;
+    public List<byte[]> getClassFileData() {
+      return classFileData;
     }
   }
 
@@ -70,14 +107,38 @@
     private String manifest;
     private Map<String, String> stringValues = new TreeMap<>();
     private Map<String, byte[]> drawables = new TreeMap<>();
+    private List<Class> classesToRemap = new ArrayList();
+
+    // Create the android resources from the passed in R classes
+    // All values will be generated based on the fields in the class.
+    // This takes the actual inner classes (e.g., R$String)
+    // These R classes will be used to rewrite the namespace and class names on the aapt2
+    // generated names.
+    AndroidTestResourceBuilder addRClassInitializeWithDefaultValues(Class... rClasses) {
+      for (Class rClass : rClasses) {
+        classesToRemap.add(rClass);
+        RClassType rClassType = RClassType.fromClass(rClass);
+        for (Field declaredField : rClass.getDeclaredFields()) {
+          String name = declaredField.getName();
+          if (rClassType == RClassType.STRING) {
+            addStringValue(name, name);
+          }
+          if (rClassType == RClassType.DRAWABLE) {
+            addDrawable(name, TINY_PNG);
+          }
+        }
+      }
+      return this;
+    }
 
     AndroidTestResourceBuilder withManifest(String manifest) {
       this.manifest = manifest;
       return this;
     }
 
-    AndroidTestResourceBuilder withSimpleManifest() {
-      this.manifest = SIMPLE_MANIFEST_WITH_STRING;
+    AndroidTestResourceBuilder withSimpleManifestAndAppNameString() {
+      this.manifest = SIMPLE_MANIFEST_WITH_APP_NAME;
+      addStringValue("app_name", "Most important app ever.");
       return this;
     }
 
@@ -110,9 +171,56 @@
       }
 
       Path output = temp.newFile("resources.zip").toPath();
-      Path rClassOutput = temp.newFolder("aapt_R_class").toPath();
-      compileWithAapt2(resFolder, manifestPath, rClassOutput, output, temp);
-      return new AndroidTestResource(new AndroidTestRClass(rClassOutput), output);
+      Path rClassOutputDir = temp.newFolder("aapt_R_class").toPath();
+      compileWithAapt2(resFolder, manifestPath, rClassOutputDir, output, temp);
+      Path rClassJavaFile =
+          Files.walk(rClassOutputDir)
+              .filter(path -> path.endsWith("R.java"))
+              .collect(MoreCollectors.onlyElement());
+      Path rClassClassFileOutput =
+          javac(CfRuntime.getDefaultCfRuntime(), temp).addSourceFiles(rClassJavaFile).compile();
+      Map<String, String> noNamespaceToProgramMap =
+          classesToRemap.stream()
+              .collect(
+                  Collectors.toMap(
+                      AndroidResourceTestingUtils::rClassWithoutNamespaceAndOuter,
+                      DescriptorUtils::getClassBinaryName));
+      List<byte[]> rewrittenRClassFiles = new ArrayList<>();
+      ZipUtils.iter(
+          rClassClassFileOutput,
+          (entry, input) -> {
+            if (ZipUtils.isClassFile(entry.getName())) {
+              rewrittenRClassFiles.add(
+                  transformer(StreamUtils.streamToByteArrayClose(input), null)
+                      .addClassTransformer(
+                          new ClassTransformer() {
+                            @Override
+                            public void visit(
+                                int version,
+                                int access,
+                                String name,
+                                String signature,
+                                String superName,
+                                String[] interfaces) {
+                              String maybeTransformedName =
+                                  isInnerRClass(name)
+                                      ? noNamespaceToProgramMap.getOrDefault(
+                                          rClassWithoutNamespaceAndOuter(name), name)
+                                      : name;
+                              super.visit(
+                                  version,
+                                  access,
+                                  maybeTransformedName,
+                                  signature,
+                                  superName,
+                                  interfaces);
+                            }
+                          })
+                      .transform());
+            }
+          });
+      return new AndroidTestResource(
+          new AndroidTestRClass(rClassJavaFile, rewrittenRClassFiles), output);
     }
 
     private String createStringResourceXml() {
diff --git a/src/test/java/com/android/tools/r8/androidresources/AndroidResourcesPassthroughTest.java b/src/test/java/com/android/tools/r8/androidresources/AndroidResourcesPassthroughTest.java
index 31359f3..41754aa 100644
--- a/src/test/java/com/android/tools/r8/androidresources/AndroidResourcesPassthroughTest.java
+++ b/src/test/java/com/android/tools/r8/androidresources/AndroidResourcesPassthroughTest.java
@@ -44,8 +44,7 @@
 
     AndroidTestResource testResource =
         new AndroidTestResourceBuilder()
-            .withSimpleManifest()
-            .addStringValue("app_name", "The App")
+            .withSimpleManifestAndAppNameString()
             .addDrawable("foo.png", AndroidResourceTestingUtils.TINY_PNG)
             .build(temp);
     Path resources = testResource.getResourceZip();
diff --git a/src/test/java/com/android/tools/r8/androidresources/RClassResourceGeneration.java b/src/test/java/com/android/tools/r8/androidresources/RClassResourceGeneration.java
new file mode 100644
index 0000000..c09a5b3
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/androidresources/RClassResourceGeneration.java
@@ -0,0 +1,108 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.androidresources;
+
+import static org.junit.Assert.assertEquals;
+
+import 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.AndroidApp;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import org.junit.BeforeClass;
+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 RClassResourceGeneration extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  private static AndroidTestResource testResource;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection parameters() {
+    return getTestParameters().withDexRuntimes().withAllApiLevels().build();
+  }
+
+  @BeforeClass
+  public static void setup() throws Exception {
+    testResource =
+        new AndroidTestResourceBuilder()
+            .withSimpleManifestAndAppNameString()
+            .addRClassInitializeWithDefaultValues(R.string.class, R.drawable.class)
+            .build(getStaticTemp());
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .setMinApi(parameters)
+        .addProgramClasses(FooBar.class)
+        .addAndroidResources(testResource)
+        .addKeepMainRule(FooBar.class)
+        .run(parameters.getRuntime(), FooBar.class)
+        // The values from the aapt2 generated R class (validated in testResourceRewriting below)
+        //    drawable:
+        //      foobar:  0x7f010000
+        //    String:
+        //      app_name 0x7f020000
+        //      foo      0x7f020001
+        //      bar      0x7f020002
+        .assertSuccessWithOutputLines("" + 0x7f010000, "" + 0x7f020001, "" + 0x7f020002)
+        .inspect(
+            codeInspector -> {
+              assertEquals(codeInspector.clazz(FooBar.class).allFields().size(), 0);
+              assertEquals(codeInspector.clazz(FooBar.class).allMethods().size(), 1);
+              assertEquals(codeInspector.allClasses().size(), 1);
+            });
+  }
+
+  @Test
+  public void testResourceRewriting() throws Exception {
+    AndroidApp resourceApp =
+        AndroidApp.builder()
+            .addClassProgramData(testResource.getRClass().getClassFileData())
+            .build();
+    CodeInspector inspector = new CodeInspector(resourceApp);
+    ClassSubject stringClazz = inspector.clazz(R.string.class);
+    // Implicitly added with the manifest
+    ensureIntFieldWithValue(stringClazz, "app_name", 0x7f020000);
+
+    ensureIntFieldWithValue(stringClazz, "bar", 0x7f020001);
+    ensureIntFieldWithValue(stringClazz, "foo", 0x7f020002);
+    ensureIntFieldWithValue(inspector.clazz(R.drawable.class), "foobar", 0x7f010000);
+  }
+
+  private void ensureIntFieldWithValue(ClassSubject clazz, String name, int value) {
+    assertEquals(clazz.field("int", name).getStaticValue().asDexValueInt().value, value);
+  }
+
+  public static class FooBar {
+
+    public static void main(String[] args) {
+      System.out.println(R.drawable.foobar);
+      System.out.println(R.string.bar);
+      System.out.println(R.string.foo);
+    }
+  }
+
+  public static class R {
+    public static class string {
+      public static int bar;
+      public static int foo;
+    }
+
+    public static class drawable {
+      public static int foobar;
+    }
+  }
+}