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;
+ }
+ }
+}