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