Add xml file tracing to enqueuer
The actual parsing and tracing of the xml is done in the
R8ResourceShrinkerState, with callbacks to the enqueuer.
Bug: b/287398085
Change-Id: I0b075ac158395830768fff654fd78e3fb381fbda
diff --git a/src/main/java/com/android/tools/r8/experimental/graphinfo/GraphEdgeInfo.java b/src/main/java/com/android/tools/r8/experimental/graphinfo/GraphEdgeInfo.java
index f645d0b..cd4e642 100644
--- a/src/main/java/com/android/tools/r8/experimental/graphinfo/GraphEdgeInfo.java
+++ b/src/main/java/com/android/tools/r8/experimental/graphinfo/GraphEdgeInfo.java
@@ -33,6 +33,7 @@
MethodHandleUseFrom,
CompanionClass,
CompanionMethod,
+ ReferencedFromXml,
Unknown
}
@@ -85,6 +86,8 @@
return "companion class for";
case CompanionMethod:
return "companion method for";
+ case ReferencedFromXml:
+ return "referenced from xml";
default:
assert false : "Unknown edge kind: " + edgeKind();
// fall through
diff --git a/src/main/java/com/android/tools/r8/graph/AppView.java b/src/main/java/com/android/tools/r8/graph/AppView.java
index abb62bf..3560ac1 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -917,10 +917,6 @@
return resourceShrinkerState;
}
- public void setResourceShrinkerState(R8ResourceShrinkerState resourceShrinkerState) {
- this.resourceShrinkerState = resourceShrinkerState;
- }
-
public boolean validateUnboxedEnumsHaveBeenPruned() {
for (DexType unboxedEnum : unboxedEnums.computeAllUnboxedEnums()) {
assert appInfo.definitionForWithoutExistenceAssert(unboxedEnum) == null
diff --git a/src/main/java/com/android/tools/r8/graph/analysis/ResourceAccessAnalysis.java b/src/main/java/com/android/tools/r8/graph/analysis/ResourceAccessAnalysis.java
index 5670096..6f13fb5 100644
--- a/src/main/java/com/android/tools/r8/graph/analysis/ResourceAccessAnalysis.java
+++ b/src/main/java/com/android/tools/r8/graph/analysis/ResourceAccessAnalysis.java
@@ -54,8 +54,6 @@
@Override
public void done(Enqueuer enqueuer) {
EnqueuerFieldAccessAnalysis.super.done(enqueuer);
- // We clear the bits here, since we will trace the final reachable entries in the second round.
- resourceShrinkerState.clearReachableBits();
}
private static boolean enabled(
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index 2a36daf..0dfda11 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -130,6 +130,7 @@
import com.android.tools.r8.kotlin.KotlinMetadataEnqueuerExtension;
import com.android.tools.r8.naming.identifiernamestring.IdentifierNameStringLookupResult;
import com.android.tools.r8.naming.identifiernamestring.IdentifierNameStringTypeLookupResult;
+import com.android.tools.r8.origin.Origin;
import com.android.tools.r8.position.Position;
import com.android.tools.r8.profile.rewriting.ProfileCollectionAdditions;
import com.android.tools.r8.shaking.AnnotationMatchResult.MatchedAnnotation;
@@ -146,6 +147,7 @@
import com.android.tools.r8.shaking.GraphReporter.KeepReasonWitness;
import com.android.tools.r8.shaking.KeepInfoCollection.MutableKeepInfoCollection;
import com.android.tools.r8.shaking.KeepMethodInfo.Joiner;
+import com.android.tools.r8.shaking.KeepReason.ReflectiveUseFromXml;
import com.android.tools.r8.shaking.RootSetUtils.ConsequentRootSet;
import com.android.tools.r8.shaking.RootSetUtils.ConsequentRootSetBuilder;
import com.android.tools.r8.shaking.RootSetUtils.RootSet;
@@ -155,6 +157,7 @@
import com.android.tools.r8.synthesis.SyntheticItems.SynthesizingContextOracle;
import com.android.tools.r8.utils.Action;
import com.android.tools.r8.utils.Box;
+import com.android.tools.r8.utils.DescriptorUtils;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.IteratorUtils;
import com.android.tools.r8.utils.OptionalBool;
@@ -510,6 +513,9 @@
? ProguardCompatibilityActions.builder()
: null;
+ if (options.isOptimizedResourceShrinking()) {
+ appView.getResourceShrinkerState().setEnqueuerCallback(this::recordReferenceFromResources);
+ }
if (mode.isTreeShaking()) {
GetArrayOfMissingTypeVerifyErrorWorkaround.register(appView, this);
InvokeVirtualToInterfaceVerifyErrorWorkaround.register(appView, this);
@@ -677,6 +683,33 @@
recordTypeReference(type, context, this::recordNonProgramClass, this::reportMissingClass);
}
+ private boolean recordReferenceFromResources(String possibleClass, Origin origin) {
+ if (!DescriptorUtils.isValidJavaType(possibleClass)) {
+ return false;
+ }
+ DexType dexType =
+ appView.dexItemFactory().createType(DescriptorUtils.javaTypeToDescriptor(possibleClass));
+ DexProgramClass clazz = appView.definitionForProgramType(dexType);
+ if (clazz != null) {
+ ReflectiveUseFromXml reason = KeepReason.reflectiveUseFromXml(origin);
+ applyMinimumKeepInfoWhenLive(
+ clazz,
+ KeepClassInfo.newEmptyJoiner()
+ .disallowMinification()
+ .disallowRepackaging()
+ .disallowOptimization());
+ markClassAsInstantiatedWithReason(clazz, reason);
+ for (ProgramMethod programInstanceInitializer : clazz.programInstanceInitializers()) {
+ // TODO(b/325884671): Only keep the actually framework targeted constructors.
+ applyMinimumKeepInfoWhenLiveOrTargeted(
+ programInstanceInitializer, KeepMethodInfo.newEmptyJoiner().disallowOptimization());
+ markMethodAsTargeted(programInstanceInitializer, reason);
+ markDirectStaticOrConstructorMethodAsLive(programInstanceInitializer, reason);
+ }
+ }
+ return clazz != null;
+ }
+
private void recordTypeReference(
DexType type,
ProgramDerivedContext context,
@@ -3747,6 +3780,9 @@
timing.begin("Finish analysis");
analyses.forEach(analyses -> analyses.done(this));
fieldAccessAnalyses.forEach(fieldAccessAnalyses -> fieldAccessAnalyses.done(this));
+ if (appView.options().isOptimizedResourceShrinking()) {
+ appView.getResourceShrinkerState().enqueuerDone(this.mode.isFinalTreeShaking());
+ }
timing.end();
assert verifyKeptGraph();
timing.begin("Finish compat building");
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepReason.java b/src/main/java/com/android/tools/r8/shaking/KeepReason.java
index 0299b05..5ab8a75 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepReason.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepReason.java
@@ -14,6 +14,7 @@
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.graph.ProgramDefinition;
import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.origin.Origin;
// TODO(herhut): Canonicalize reason objects.
public abstract class KeepReason {
@@ -83,6 +84,10 @@
return new ReflectiveUseFrom(method.getDefinition());
}
+ public static ReflectiveUseFromXml reflectiveUseFromXml(Origin origin) {
+ return new ReflectiveUseFromXml(origin);
+ }
+
public static KeepReason methodHandleReferencedIn(ProgramMethod method) {
return new MethodHandleReferencedFrom(method.getDefinition());
}
@@ -299,6 +304,61 @@
}
}
+ public static class ReflectiveUseFromXml extends KeepReason {
+
+ private final Origin origin;
+
+ private ReflectiveUseFromXml(Origin origin) {
+ this.origin = origin;
+ }
+
+ @Override
+ public boolean isDueToReflectiveUse() {
+ return true;
+ }
+
+ @Override
+ public EdgeKind edgeKind() {
+ return EdgeKind.ReferencedFromXml;
+ }
+
+ @Override
+ public GraphNode getSourceNode(GraphReporter graphReporter) {
+ return new XmlGraphNode(origin);
+ }
+
+ private static class XmlGraphNode extends GraphNode {
+
+ private final Origin origin;
+
+ public XmlGraphNode(Origin origin) {
+ super(false);
+ this.origin = origin;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (!(o instanceof XmlGraphNode)) {
+ return false;
+ }
+ return ((XmlGraphNode) o).origin.equals(this.origin);
+ }
+
+ @Override
+ public int hashCode() {
+ return origin.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return origin.toString();
+ }
+ }
+ }
+
private static class MethodHandleReferencedFrom extends BasedOnOtherMethod {
private MethodHandleReferencedFrom(DexEncodedMethod method) {
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/ResourceTableUtil.kt b/src/resourceshrinker/java/com/android/build/shrinker/ResourceTableUtil.kt
index 69952ef..4327997 100644
--- a/src/resourceshrinker/java/com/android/build/shrinker/ResourceTableUtil.kt
+++ b/src/resourceshrinker/java/com/android/build/shrinker/ResourceTableUtil.kt
@@ -93,7 +93,7 @@
val entry: Resources.Entry
)
-private fun toIdentifier(
+fun toIdentifier(
resourcePackage: Resources.Package,
type: Resources.Type,
entry: Resources.Entry
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 2392218..d8c98c7 100644
--- a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java
+++ b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java
@@ -5,11 +5,16 @@
import static com.android.build.shrinker.r8integration.LegacyResourceShrinker.getUtfReader;
+import com.android.aapt.Resources;
import com.android.aapt.Resources.ConfigValue;
import com.android.aapt.Resources.Entry;
+import com.android.aapt.Resources.FileReference;
import com.android.aapt.Resources.Item;
+import com.android.aapt.Resources.Package;
import com.android.aapt.Resources.ResourceTable;
import com.android.aapt.Resources.Value;
+import com.android.aapt.Resources.XmlAttribute;
+import com.android.aapt.Resources.XmlElement;
import com.android.aapt.Resources.XmlNode;
import com.android.build.shrinker.NoDebugReporter;
import com.android.build.shrinker.ResourceShrinkerImplKt;
@@ -26,15 +31,19 @@
import com.android.ide.common.resources.usage.ResourceUsageModel.Resource;
import com.android.resources.ResourceType;
import com.android.tools.r8.FeatureSplit;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.origin.PathOrigin;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import java.io.IOException;
import java.io.InputStream;
-import java.util.Collections;
+import java.nio.file.Paths;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@@ -48,18 +57,32 @@
private Supplier<InputStream> manifestProvider;
private final Map<String, Supplier<InputStream>> resfileProviders = new HashMap<>();
private final Map<ResourceTable, FeatureSplit> resourceTables = new HashMap<>();
+ private ClassReferenceCallback enqueuerCallback;
+ private Map<Integer, String> resourceIdToXmlFiles;
+ private Set<String> packageNames;
+ private final Set<String> seenNoneClassValues = new HashSet<>();
+ private final Set<Integer> seenResourceIds = new HashSet<>();
+
+ @FunctionalInterface
+ public interface ClassReferenceCallback {
+ boolean tryClass(String possibleClass, Origin xmlFileOrigin);
+ }
public R8ResourceShrinkerState(Function<Exception, RuntimeException> errorHandler) {
r8ResourceShrinkerModel = new R8ResourceShrinkerModel(NoDebugReporter.INSTANCE, true);
this.errorHandler = errorHandler;
}
- public List<String> trace(int id) {
+ public void trace(int id) {
+ if (!seenResourceIds.add(id)) {
+ return;
+ }
Resource resource = r8ResourceShrinkerModel.getResourceStore().getResource(id);
if (resource == null) {
- return Collections.emptyList();
+ return;
}
ResourceUsageModel.markReachable(resource);
+ traceXml(id);
if (resource.references != null) {
for (Resource reference : resource.references) {
if (!reference.isReachable()) {
@@ -67,7 +90,25 @@
}
}
}
- return Collections.emptyList();
+ }
+
+ public void setEnqueuerCallback(ClassReferenceCallback enqueuerCallback) {
+ assert this.enqueuerCallback == null;
+ this.enqueuerCallback = enqueuerCallback;
+ }
+
+ private synchronized Set<String> getPackageNames() {
+ // TODO(b/325888516): Consider only doing this for the package corresponding to the current
+ // feature.
+ if (packageNames == null) {
+ packageNames = new HashSet<>();
+ for (ResourceTable resourceTable : resourceTables.keySet()) {
+ for (Package aPackage : resourceTable.getPackageList()) {
+ packageNames.add(aPackage.getPackageName());
+ }
+ }
+ }
+ return packageNames;
}
public void setManifestProvider(Supplier<InputStream> manifestProvider) {
@@ -144,6 +185,74 @@
return resEntriesToKeep.build();
}
+ private void traceXml(int id) {
+ String xmlFile = getResourceIdToXmlFiles().get(id);
+ if (xmlFile != null) {
+ InputStream inputStream = xmlFileProviders.get(xmlFile).get();
+ try {
+ XmlNode xmlNode = XmlNode.parseFrom(inputStream);
+ visitNode(xmlNode, xmlFile);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ private void tryEnqueuerOnString(String possibleClass, String xmlName) {
+ // There are a lot of xml tags and attributes that are evaluated over and over, if it is
+ // not a class, ignore it.
+ if (seenNoneClassValues.contains(possibleClass)) {
+ return;
+ }
+ if (!enqueuerCallback.tryClass(possibleClass, new PathOrigin(Paths.get(xmlName)))) {
+ seenNoneClassValues.add(possibleClass);
+ }
+ }
+
+ private void visitNode(XmlNode xmlNode, String xmlName) {
+ XmlElement element = xmlNode.getElement();
+ tryEnqueuerOnString(element.getName(), xmlName);
+ for (XmlAttribute xmlAttribute : element.getAttributeList()) {
+ String value = xmlAttribute.getValue();
+ tryEnqueuerOnString(value, xmlName);
+ if (value.startsWith(".")) {
+ // package specific names, e.g. context
+ getPackageNames().forEach(s -> tryEnqueuerOnString(s + value, xmlName));
+ }
+ }
+ element.getChildList().forEach(e -> visitNode(e, xmlName));
+ }
+
+ public Map<Integer, String> getResourceIdToXmlFiles() {
+ if (resourceIdToXmlFiles == null) {
+ resourceIdToXmlFiles = new HashMap<>();
+ for (ResourceTable resourceTable : resourceTables.keySet()) {
+ for (Package packageEntry : resourceTable.getPackageList()) {
+ for (Resources.Type type : packageEntry.getTypeList()) {
+ for (Entry entry : type.getEntryList()) {
+ for (ConfigValue configValue : entry.getConfigValueList()) {
+ if (configValue.hasValue()) {
+ Value value = configValue.getValue();
+ if (value.hasItem()) {
+ Item item = value.getItem();
+ if (item.hasFile()) {
+ FileReference file = item.getFile();
+ if (file.getType() == FileReference.Type.PROTO_XML) {
+ int id = ResourceTableUtilKt.toIdentifier(packageEntry, type, entry);
+ resourceIdToXmlFiles.put(id, file.getPath());
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return resourceIdToXmlFiles;
+ }
+
private List<Integer> getResourcesToRemove() {
return r8ResourceShrinkerModel.getResourceStore().getResources().stream()
.filter(r -> !r.isReachable() && !r.isPublic())
@@ -171,6 +280,16 @@
}
}
+ public void enqueuerDone(boolean isFinalTreeshaking) {
+ enqueuerCallback = null;
+ seenResourceIds.clear();
+ if (!isFinalTreeshaking) {
+ // After final tree shaking we will need the reachability bits to decide what to write out
+ // from the model.
+ clearReachableBits();
+ }
+ }
+
public void clearReachableBits() {
for (Resource resource : r8ResourceShrinkerModel.getResourceStore().getResources()) {
resource.setReachable(false);
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 5040ed3..b4a170a 100644
--- a/src/test/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java
+++ b/src/test/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java
@@ -272,6 +272,7 @@
private final Map<String, String> xmlFiles = new TreeMap<>();
private final List<Class<?>> classesToRemap = new ArrayList<>();
private int packageId = 0x7f;
+ private String packageName;
// Create the android resources from the passed in R classes
// All values will be generated based on the fields in the class.
@@ -334,6 +335,11 @@
return this;
}
+ public AndroidTestResourceBuilder addXml(String name, String content) {
+ xmlFiles.put(name, content);
+ return this;
+ }
+
AndroidTestResourceBuilder addExtraLanguageString(String name) {
stringValuesWithExtraLanguage.add(name);
return this;
@@ -354,6 +360,11 @@
return this;
}
+ public AndroidTestResourceBuilder setPackageName(String packageName) {
+ this.packageName = packageName;
+ return this;
+ }
+
public AndroidTestResource build(TemporaryFolder temp) throws IOException {
Path manifestPath =
FileUtils.writeTextFile(temp.newFile("AndroidManifest.xml").toPath(), this.manifest);
@@ -391,7 +402,10 @@
Path output = temp.newFile("resources.zip").toPath();
Path rClassOutputDir = temp.newFolder("aapt_R_class").toPath();
- compileWithAapt2(resFolder, manifestPath, rClassOutputDir, output, temp, packageId);
+ String aaptPackageName =
+ packageName != null ? packageName : "thepackage" + packageId + ".foobar";
+ compileWithAapt2(
+ resFolder, manifestPath, rClassOutputDir, output, temp, packageId, aaptPackageName);
Path rClassJavaFile =
Files.walk(rClassOutputDir)
.filter(path -> path.endsWith("R.java"))
@@ -528,7 +542,8 @@
Path rClassFolder,
Path resourceZip,
TemporaryFolder temp,
- int packageId)
+ int packageId,
+ String packageName)
throws IOException {
Path compileOutput = temp.newFile("compiled.zip").toPath();
ProcessResult compileProcessResult =
@@ -552,7 +567,7 @@
"" + packageId,
"--allow-reserved-package-id",
"--rename-resources-package",
- "thepackage" + packageId + ".foobar",
+ packageName,
"--proto-format",
compileOutput.toString());
failOnError(linkProcesResult);
diff --git a/src/test/java/com/android/tools/r8/androidresources/DeadCodeEliminatedXmlReferenceTest.java b/src/test/java/com/android/tools/r8/androidresources/DeadCodeEliminatedXmlReferenceTest.java
new file mode 100644
index 0000000..6afb424
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/androidresources/DeadCodeEliminatedXmlReferenceTest.java
@@ -0,0 +1,90 @@
+// 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.androidresources;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+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.androidresources.AndroidResourceTestingUtils.AndroidTestResource;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResourceBuilder;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+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 DeadCodeEliminatedXmlReferenceTest extends TestBase {
+
+ @Parameter(0)
+ public TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection parameters() {
+ return getTestParameters().withDefaultDexRuntime().withAllApiLevels().build();
+ }
+
+ public static String VIEW_WITH_CLASS_ATTRIBUTE_REFERENCE =
+ "<view xmlns:android=\"http://schemas.android.com/apk/res/android\" class=\"%s\"/>\n";
+
+ public static AndroidTestResource getTestResources(TemporaryFolder temp, String xmlFile)
+ throws Exception {
+ return new AndroidTestResourceBuilder()
+ .withSimpleManifestAndAppNameString()
+ .addRClassInitializeWithDefaultValues(R.xml.class)
+ .addXml(
+ "xml_with_bar_reference.xml",
+ String.format(VIEW_WITH_CLASS_ATTRIBUTE_REFERENCE, Bar.class.getTypeName()))
+ .build(temp);
+ }
+
+ @Test
+ public void testDeadReference() throws Exception {
+ String formatedXmlFile =
+ String.format(VIEW_WITH_CLASS_ATTRIBUTE_REFERENCE, Bar.class.getTypeName());
+ testForR8(parameters.getBackend())
+ .setMinApi(parameters)
+ .addProgramClasses(TestClass.class, Bar.class)
+ .addAndroidResources(getTestResources(temp, formatedXmlFile))
+ .addKeepMainRule(TestClass.class)
+ .enableOptimizedShrinking()
+ .compile()
+ .inspectShrunkenResources(
+ resourceTableInspector -> {
+ resourceTableInspector.assertDoesNotContainResourceWithName(
+ "xml", "xml_with_bar_reference");
+ })
+ .run(parameters.getRuntime(), TestClass.class)
+ .inspect(
+ codeInspector -> {
+ ClassSubject barClass = codeInspector.clazz(Bar.class);
+ assertThat(barClass, isAbsent());
+ })
+ .assertSuccess();
+ }
+
+ public static class TestClass {
+ public static void main(String[] args) {
+ // Reference only the xml
+ int i = 42;
+ if (i == 43) {
+ System.out.println(R.xml.xml_with_bar_reference);
+ }
+ }
+ }
+
+ // Only referenced from XML file
+ public static class Bar {}
+
+ public static class R {
+ public static class xml {
+ public static int xml_with_bar_reference;
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/androidresources/NestedXmlReferences.java b/src/test/java/com/android/tools/r8/androidresources/NestedXmlReferences.java
new file mode 100644
index 0000000..70b6fbe
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/androidresources/NestedXmlReferences.java
@@ -0,0 +1,103 @@
+// 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.androidresources;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed;
+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.androidresources.AndroidResourceTestingUtils.AndroidTestResource;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResourceBuilder;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+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 NestedXmlReferences extends TestBase {
+
+ @Parameter(0)
+ public TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection parameters() {
+ return getTestParameters().withDefaultDexRuntime().withAllApiLevels().build();
+ }
+
+ public static String TO_BE_INCLUDED =
+ "<view xmlns:android=\"http://schemas.android.com/apk/res/android\" class=\"%s\"/>\n";
+
+ public static String INCLUDING_OTHER_XML =
+ "<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n"
+ + " android:orientation=\"vertical\"\n"
+ + " android:layout_width=\"match_parent\"\n"
+ + " android:layout_height=\"match_parent\"\n"
+ + " android:gravity=\"center_horizontal\""
+ + " class=\"%s\">\n"
+ + "\n"
+ + " <include layout=\"@xml/to_be_included\"/>\n"
+ + "\n"
+ + " <TextView android:layout_width=\"match_parent\"\n"
+ + " android:layout_height=\"wrap_content\"\n"
+ + " android:padding=\"10dp\" />\n"
+ + "</LinearLayout>";
+
+ public static AndroidTestResource getTestResources(TemporaryFolder temp) throws Exception {
+ return new AndroidTestResourceBuilder()
+ .withSimpleManifestAndAppNameString()
+ .addRClassInitializeWithDefaultValues(R.xml.class)
+ .addXml("to_be_included.xml", String.format(TO_BE_INCLUDED, Foo.class.getTypeName()))
+ .addXml(
+ "including_other_xml.xml", String.format(INCLUDING_OTHER_XML, Bar.class.getTypeName()))
+ .build(temp);
+ }
+
+ @Test
+ public void testTransitiveReference() throws Exception {
+ testForR8(parameters.getBackend())
+ .setMinApi(parameters)
+ .addProgramClasses(TestClass.class, Bar.class, Foo.class)
+ .addAndroidResources(getTestResources(temp))
+ .addKeepMainRule(TestClass.class)
+ .enableOptimizedShrinking()
+ .compile()
+ .inspectShrunkenResources(
+ resourceTableInspector -> {
+ resourceTableInspector.assertContainsResourceWithName("xml", "including_other_xml");
+ resourceTableInspector.assertContainsResourceWithName("xml", "to_be_included");
+ })
+ .run(parameters.getRuntime(), TestClass.class)
+ .inspect(
+ codeInspector -> {
+ ClassSubject barClass = codeInspector.clazz(Bar.class);
+ assertThat(barClass, isPresentAndNotRenamed());
+ ClassSubject fooClass = codeInspector.clazz(Foo.class);
+ assertThat(fooClass, isPresentAndNotRenamed());
+ })
+ .assertSuccess();
+ }
+
+ public static class TestClass {
+ public static void main(String[] args) {
+ // Reference only the xml
+ System.out.println(R.xml.including_other_xml);
+ }
+ }
+
+ public static class Bar {}
+
+ public static class Foo {}
+
+ public static class R {
+ public static class xml {
+ public static int to_be_included;
+ public static int including_other_xml;
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/androidresources/XmlFilesWithClassReferences.java b/src/test/java/com/android/tools/r8/androidresources/XmlFilesWithClassReferences.java
new file mode 100644
index 0000000..e69669b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/androidresources/XmlFilesWithClassReferences.java
@@ -0,0 +1,149 @@
+// 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.androidresources;
+
+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.isPresentAndNotRenamed;
+import static org.hamcrest.MatcherAssert.assertThat;
+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.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+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 XmlFilesWithClassReferences extends TestBase {
+
+ @Parameter(0)
+ public TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection parameters() {
+ return getTestParameters().withDefaultDexRuntime().withAllApiLevels().build();
+ }
+
+ public static String VIEW_WITH_CLASS_ATTRIBUTE_REFERENCE =
+ "<view xmlns:android=\"http://schemas.android.com/apk/res/android\" class=\"%s\"/>\n";
+
+ public static String TRANSITION_WITH_CLASS_ATTRIBUTE =
+ "<transitionSet>\n" + " <transition class=\"%s\"/>\n" + "</transitionSet>\n";
+
+ public static String CHANGE_BOUNDS_WITH_CLASS_ATTRIBUTE =
+ "<changeBounds>\n" + " <pathMotion class=\"%s\"/>\n" + "</changeBounds>";
+
+ public static String FRAGMENT_CONTAINER_VIEW_WITH_CLASS_ATTRIBUTE =
+ "<androidx.fragment.app.FragmentContainerView class=\"%s\"/>";
+
+ public static String FRAGMENT_WITH_CLASS_ATTRIBUTE = "<fragment class=\"%s\"/>";
+
+ public static AndroidTestResource getTestResources(TemporaryFolder temp, String xmlFile)
+ throws Exception {
+ return new AndroidTestResourceBuilder()
+ .withSimpleManifestAndAppNameString()
+ .addRClassInitializeWithDefaultValues(R.xml.class)
+ .addXml("xml_with_bar_reference.xml", xmlFile)
+ .build(temp);
+ }
+
+ @Test
+ public void testFragmentContainerWithClassAttributeReference() throws Exception {
+ testXmlReferenceWithBarClassInserted(FRAGMENT_CONTAINER_VIEW_WITH_CLASS_ATTRIBUTE, false);
+ }
+
+ @Test
+ public void testViewWithClassAttributeReference() throws Exception {
+ testXmlReferenceWithBarClassInserted(VIEW_WITH_CLASS_ATTRIBUTE_REFERENCE, false);
+ }
+
+ @Test
+ public void testTransitionWithClassAttributeReference() throws Exception {
+ testXmlReferenceWithBarClassInserted(TRANSITION_WITH_CLASS_ATTRIBUTE, false);
+ }
+
+ @Test
+ public void testChangeBoundsWithClassAttributeReference() throws Exception {
+ testXmlReferenceWithBarClassInserted(CHANGE_BOUNDS_WITH_CLASS_ATTRIBUTE, false);
+ }
+
+ @Test
+ public void testFragmentWithClassAttributeReference() throws Exception {
+ testXmlReferenceWithBarClassInserted(FRAGMENT_WITH_CLASS_ATTRIBUTE, false);
+ }
+
+ public void testXmlReferenceWithBarClassInserted(String xmlFile, boolean assertFoo)
+ throws Exception {
+ String formatedXmlFile = String.format(xmlFile, Bar.class.getTypeName());
+ testForR8(parameters.getBackend())
+ .setMinApi(parameters)
+ .addProgramClasses(TestClass.class, Bar.class, BarFoo.class)
+ .addAndroidResources(getTestResources(temp, formatedXmlFile))
+ .addKeepMainRule(TestClass.class)
+ .enableOptimizedShrinking()
+ .compile()
+ .inspectShrunkenResources(
+ resourceTableInspector -> {
+ resourceTableInspector.assertContainsResourceWithName(
+ "xml", "xml_with_bar_reference");
+ })
+ .run(parameters.getRuntime(), TestClass.class)
+ .inspect(
+ codeInspector -> {
+ ClassSubject barClass = codeInspector.clazz(Bar.class);
+ assertThat(barClass, isPresentAndNotRenamed());
+ // We should have two and only two methods, the two constructors.
+ assertEquals(barClass.allMethods(MethodSubject::isInstanceInitializer).size(), 2);
+ assertEquals(barClass.allMethods().size(), 2);
+ assertThat(codeInspector.clazz(BarFoo.class), assertFoo ? isPresent() : isAbsent());
+ })
+ .assertSuccess();
+ }
+
+ public static class TestClass {
+ public static void main(String[] args) {
+ if (System.currentTimeMillis() == 0) {
+ // Reference only the xml
+ System.out.println(R.xml.xml_with_bar_reference);
+ }
+ }
+ }
+
+ // Only referenced from XML file
+ public static class Bar {
+ public Bar() {
+ System.out.println("init");
+ }
+
+ public Bar(String x) {
+ System.out.println("init with string");
+ }
+
+ public void foo() {
+ System.out.println("foo");
+ }
+
+ public static void bar() {
+ System.out.println("bar");
+ }
+ }
+
+ public static class BarFoo {}
+
+ public static class R {
+ public static class xml {
+ public static int xml_with_bar_reference;
+ public static int xml_with_foo_reference;
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/androidresources/XmlFilesWithReferences.java b/src/test/java/com/android/tools/r8/androidresources/XmlFilesWithReferences.java
index 9f80fe1..b55edff 100644
--- a/src/test/java/com/android/tools/r8/androidresources/XmlFilesWithReferences.java
+++ b/src/test/java/com/android/tools/r8/androidresources/XmlFilesWithReferences.java
@@ -30,7 +30,7 @@
return new AndroidTestResourceBuilder()
.withSimpleManifestAndAppNameString()
.addRClassInitializeWithDefaultValues(R.string.class, R.xml.class)
- .addXmlWithStringReference("foo_with_reference", "referenced_from_xml")
+ .addXmlWithStringReference("foo_with_reference.xml", "referenced_from_xml")
.build(temp);
}
diff --git a/src/test/java/com/android/tools/r8/androidresources/xmltags/Bar.java b/src/test/java/com/android/tools/r8/androidresources/xmltags/Bar.java
new file mode 100644
index 0000000..4abb704
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/androidresources/xmltags/Bar.java
@@ -0,0 +1,6 @@
+// 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.androidresources.xmltags;
+
+public class Bar {}
diff --git a/src/test/java/com/android/tools/r8/androidresources/xmltags/Foo.java b/src/test/java/com/android/tools/r8/androidresources/xmltags/Foo.java
new file mode 100644
index 0000000..7e5972e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/androidresources/xmltags/Foo.java
@@ -0,0 +1,10 @@
+// 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.androidresources.xmltags;
+
+public class Foo {
+ public Foo() {
+ System.out.println(R.xml.xml_with_bar_reference);
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/androidresources/xmltags/R.java b/src/test/java/com/android/tools/r8/androidresources/xmltags/R.java
new file mode 100644
index 0000000..f564884
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/androidresources/xmltags/R.java
@@ -0,0 +1,11 @@
+// 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.androidresources.xmltags;
+
+public class R {
+ public static class xml {
+ public static int xml_with_bar_reference;
+ public static int xml_with_foo_reference;
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/androidresources/xmltags/XmlTagClassReferenceTest.java b/src/test/java/com/android/tools/r8/androidresources/xmltags/XmlTagClassReferenceTest.java
new file mode 100644
index 0000000..4821808
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/androidresources/xmltags/XmlTagClassReferenceTest.java
@@ -0,0 +1,134 @@
+// 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.androidresources.xmltags;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentIf;
+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.androidresources.AndroidResourceTestingUtils.AndroidTestResource;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResourceBuilder;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class XmlTagClassReferenceTest extends TestBase {
+
+ public static final String xmlWithBarReference = "xml_with_bar_reference";
+ public static final String xmlWithFooReference = "xml_with_foo_reference";
+
+ public static final List<String> allXml =
+ ImmutableList.of(xmlWithBarReference, xmlWithFooReference);
+
+ public static final List<Class> allClasses = ImmutableList.of(Foo.class, Bar.class);
+
+ @Parameter(0)
+ public TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection parameters() {
+ return getTestParameters().withDefaultDexRuntime().withAllApiLevels().build();
+ }
+
+ public static String NO_REF_XML = "<Z/>";
+
+ public static String XML_WITH_DIRECT_FOO_REFERENCE = "<" + Foo.class.getTypeName() + "/>";
+
+ public static String XML_WITH_DIRECT_BAR_REFERENCE = "<" + Bar.class.getTypeName() + "/>";
+
+ public static String XML_NO_PACKAGE =
+ "<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n"
+ + " xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n"
+ + " android:layout_width=\"match_parent\"\n"
+ + " android:layout_height=\"match_parent\">\n"
+ + "\n"
+ + " <view class=\".Foo\"\n"
+ + " android:layout_width=\"300dp\"\n"
+ + " android:layout_height=\"300dp\"\n"
+ + " android:paddingLeft=\"20dp\"\n"
+ + " android:paddingBottom=\"40dp\"\n/>"
+ + "\n"
+ + "</FrameLayout>";
+
+ public static AndroidTestResource getTestResources(
+ TemporaryFolder temp, String xmlForXmlWithBar, String xmlForXmlWithFoo) throws Exception {
+ return new AndroidTestResourceBuilder()
+ .withSimpleManifestAndAppNameString()
+ .setPackageName(R.class.getPackage().getName())
+ .addRClassInitializeWithDefaultValues(R.xml.class)
+ .addXml(xmlWithFooReference + ".xml", xmlForXmlWithFoo)
+ .addXml(xmlWithBarReference + ".xml", xmlForXmlWithBar)
+ .build(temp);
+ }
+
+ @Test
+ public void testTextXml() throws Exception {
+ test(
+ getTestResources(temp, NO_REF_XML, XML_WITH_DIRECT_FOO_REFERENCE),
+ ImmutableList.of(Foo.class),
+ allXml);
+ }
+
+ @Test
+ public void testTextXmlNoPackagePrefix() throws Exception {
+ test(getTestResources(temp, NO_REF_XML, XML_NO_PACKAGE), ImmutableList.of(Foo.class), allXml);
+ }
+
+ @Test
+ public void testTransitiveReferences() throws Exception {
+ test(
+ getTestResources(temp, XML_WITH_DIRECT_BAR_REFERENCE, XML_WITH_DIRECT_FOO_REFERENCE),
+ allClasses,
+ allXml);
+ }
+
+ public void test(
+ AndroidTestResource androidTestResource,
+ List<Class> presentClass,
+ List<String> presentXmlFiles)
+ throws Exception {
+ testForR8(parameters.getBackend())
+ .setMinApi(parameters)
+ .addProgramClasses(TestClass.class, Foo.class, Bar.class)
+ .addAndroidResources(androidTestResource)
+ .addKeepMainRule(TestClass.class)
+ .enableOptimizedShrinking()
+ .compile()
+ .inspectShrunkenResources(
+ resourceTableInspector -> {
+ for (String xml : allXml) {
+ if (presentXmlFiles.contains(xml)) {
+ resourceTableInspector.assertContainsResourceWithName("xml", xml);
+ } else {
+ resourceTableInspector.assertDoesNotContainResourceWithName("xml", xml);
+ }
+ }
+ })
+ .run(parameters.getRuntime(), TestClass.class)
+ .inspect(
+ codeInspector -> {
+ for (Class clazz : allClasses) {
+ assertThat(codeInspector.clazz(clazz), isPresentIf(presentClass.contains(clazz)));
+ }
+ })
+ .assertSuccess();
+ }
+
+ public static class TestClass {
+ public static void main(String[] args) {
+ if (System.currentTimeMillis() == 0) {
+ // Reference only the xml
+ System.out.println(R.xml.xml_with_foo_reference);
+ }
+ }
+ }
+}