Ignore resource types that are invalid for resource shrinking
ResourceType.fromClassName returns null for synthetic types, see:
https://android.googlesource.com/platform/tools/base/+/mirror-goog-studio-main/layoutlib-api/src/main/java/com/android/resources/ResourceType.java#100
and above
In the old shrinker we explicitly only added entries when fromClassName returned non null
Regression test uses the ResourceTable builder to add the synthetic type that was causing this NPE.
Bug: b/386569094
Change-Id: I6a0721424293fff737dc722a1b78b959fec12e88
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java
index 4740a7c..1165be4 100644
--- a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java
+++ b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java
@@ -465,7 +465,8 @@
Entry entry = entryWrapper.getEntry();
int entryId = entryWrapper.getId();
recordSingleValueResources(resourceType, entry, entryId);
- if (resourceType != ResourceType.STYLEABLE || includeStyleables) {
+ if (resourceType != null
+ && (resourceType != ResourceType.STYLEABLE || includeStyleables)) {
this.addResource(
resourceType,
entryWrapper.getPackageName(),
diff --git a/src/test/java/com/android/tools/r8/androidresources/SyntheticResourceTypeTest.java b/src/test/java/com/android/tools/r8/androidresources/SyntheticResourceTypeTest.java
new file mode 100644
index 0000000..39f81a6
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/androidresources/SyntheticResourceTypeTest.java
@@ -0,0 +1,88 @@
+// Copyright (c) 2025, 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.aapt.Resources.ResourceTable;
+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 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 SyntheticResourceTypeTest extends TestBase {
+
+ @Parameter(0)
+ public TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection parameters() {
+ return getTestParameters().withDefaultDexRuntime().withAllApiLevels().build();
+ }
+
+ public static AndroidTestResource getTestResources(TemporaryFolder temp) throws Exception {
+ return new AndroidTestResourceBuilder()
+ .withSimpleManifestAndAppNameString()
+ .addRClassInitializeWithDefaultValues(R.drawable.class)
+ .build(temp)
+ .mangleResourceTable(
+ resourceTable -> {
+ // We are adding a synthetic type that we can't easily get aapt2 to generate.
+ // Explicitly build the entry using the resource table builder.
+ ResourceTable.Builder resourceTableBuilder = resourceTable.toBuilder();
+ resourceTableBuilder
+ .getPackageBuilderList()
+ .get(0)
+ .addTypeBuilder()
+ .setName("macro")
+ .addEntryBuilder()
+ .setName("macro_name")
+ .addConfigValueBuilder()
+ .getValueBuilder()
+ .getItemBuilder()
+ .getStrBuilder()
+ .setValue("macro_value")
+ .build();
+ return resourceTableBuilder.build();
+ });
+ }
+
+ @Test
+ public void testR8() throws Exception {
+ testForR8(parameters.getBackend())
+ .setMinApi(parameters)
+ .addProgramClasses(FooBar.class)
+ .addAndroidResources(getTestResources(temp))
+ .addKeepMainRule(FooBar.class)
+ .enableOptimizedShrinking()
+ .compile()
+ .inspectShrunkenResources(
+ resourceTableInspector -> {
+ resourceTableInspector.assertContainsResourceWithName("drawable", "foo");
+ // The synthetic macro entries should not be touched by the resource shrinker.
+ resourceTableInspector.assertContainsResourceWithName("macro", "macro_name");
+ resourceTableInspector.assertDoesNotContainResourceWithName("drawable", "bar");
+ });
+ }
+
+ public static class FooBar {
+ public static void main(String[] args) {
+ System.out.println(R.drawable.foo);
+ }
+ }
+
+ public static class R {
+ public static class drawable {
+ public static int foo;
+ public static int bar;
+ }
+ }
+}
diff --git a/src/test/testbase/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java b/src/test/testbase/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java
index 92e0618..d55db2f 100644
--- a/src/test/testbase/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java
+++ b/src/test/testbase/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java
@@ -32,6 +32,7 @@
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Collection;
@@ -42,6 +43,7 @@
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
+import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@@ -175,7 +177,7 @@
public static class AndroidTestResource {
private final AndroidTestRClass rClass;
- private final Path resourceZip;
+ private Path resourceZip;
private final List<String> additionalKeepRuleFiles;
AndroidTestResource(
@@ -185,6 +187,28 @@
this.additionalKeepRuleFiles = additionalRawXmlFiles;
}
+ public AndroidTestResource mangleResourceTable(Function<ResourceTable, ResourceTable> mapper)
+ throws IOException {
+ Path newName = Paths.get(resourceZip.toString() + ".mangled.ap_");
+ ZipUtils.map(
+ resourceZip,
+ newName,
+ (zipEntry, bytes) -> {
+ if (zipEntry.getName().equals("resources.pb")) {
+ try {
+ ResourceTable resourceTable = ResourceTable.parseFrom(bytes);
+ ResourceTable converted = mapper.apply(resourceTable);
+ return converted.toByteArray();
+ } catch (InvalidProtocolBufferException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return bytes;
+ });
+ resourceZip = newName;
+ return this;
+ }
+
public AndroidTestRClass getRClass() {
return rClass;
}