Ensure that we keep resource table entries for overlapping res entries

This solves the issue that when we have
base: res/xml/foo.xml
and
feature: res/xml/foo.xml
we will keep both files when writing, even if only one of these are
reachable through a resource table entry (in either base or feature)

This ensures that we keep the resource table entry.

Bug: b/384905036
Change-Id: I7d16826ad522664e5d98c000e9e9df64378a0484
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 3741cb5..6f99720 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -990,11 +990,9 @@
         LegacyResourceShrinker shrinker = resourceShrinkerBuilder.build();
         shrinkerResult = shrinker.run();
       }
-      Set<String> toKeep = shrinkerResult.getResFolderEntriesToKeep();
       writeResourcesToConsumer(
           reporter,
           shrinkerResult,
-          toKeep,
           options.androidResourceProvider,
           options.androidResourceConsumer,
           FeatureSplit.BASE);
@@ -1004,7 +1002,6 @@
             writeResourcesToConsumer(
                 reporter,
                 shrinkerResult,
-                toKeep,
                 featureSplit.getAndroidResourceProvider(),
                 featureSplit.getAndroidResourceConsumer(),
                 featureSplit);
@@ -1019,7 +1016,6 @@
   private static void writeResourcesToConsumer(
       Reporter reporter,
       ShrinkerResult shrinkerResult,
-      Set<String> toKeep,
       AndroidResourceProvider androidResourceProvider,
       AndroidResourceConsumer androidResourceConsumer,
       FeatureSplit featureSplit)
@@ -1044,7 +1040,9 @@
           break;
         case RES_FOLDER_FILE:
         case XML_FILE:
-          if (toKeep.contains(androidResource.getPath().location())) {
+          if (shrinkerResult
+              .getResFolderEntriesToKeep()
+              .contains(androidResource.getPath().location())) {
             androidResourceConsumer.accept(
                 new R8PassThroughAndroidResource(androidResource, reporter), reporter);
           }
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinkerImpl.kt b/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinkerImpl.kt
index d404e2e6..b13a28b 100644
--- a/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinkerImpl.kt
+++ b/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinkerImpl.kt
@@ -307,16 +307,12 @@
     return isJarPathReachable(folder, name);
 }
 
-private fun ResourceStore.getResourceId(
-    folder: String,
-    name: String
-): Int {
-    val folderType = ResourceFolderType.getFolderType(folder) ?: return -1
+fun ResourceStore.getResourcesFor(path: String): List<Resource> {
+    val (_, folder, name) = path.split('/', limit = 3)
+    val folderType = ResourceFolderType.getFolderType(folder) ?: return emptyList()
     val resourceName = name.substringBefore('.')
     return FolderTypeRelationship.getRelatedResourceTypes(folderType)
         .filterNot { it == ResourceType.ID }
         .flatMap { getResources(it, resourceName) }
-        .map { it.value }
-        .getOrElse(0) { -1 }
-
+        .toList()
 }
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/LegacyResourceShrinker.java b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/LegacyResourceShrinker.java
index 6cbc8c9..a64a3fd 100644
--- a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/LegacyResourceShrinker.java
+++ b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/LegacyResourceShrinker.java
@@ -101,7 +101,7 @@
     public Builder addResFolderInput(String path, byte[] bytes) {
       PathAndBytes existing = resFolderInputs.get(path);
       if (existing != null) {
-        assert Arrays.equals(existing.getBytes(), bytes);
+        existing.setDuplicated(true);
       } else {
         resFolderInputs.put(path, new PathAndBytes(bytes, path));
       }
@@ -111,7 +111,7 @@
     public Builder addXmlInput(String path, byte[] bytes) {
       PathAndBytes existing = xmlInputs.get(path);
       if (existing != null) {
-        assert Arrays.equals(existing.getBytes(), bytes);
+        existing.setDuplicated(true);
       } else {
         xmlInputs.put(path, new PathAndBytes(bytes, path));
       }
@@ -217,14 +217,20 @@
               debugReporter.debug(() -> "The root reachable resources are:");
               roots.forEach(root -> debugReporter.debug(() -> " " + root));
             });
-    debugReporter.debug(() -> "Unused resources are: ");
-    unusedResources.forEach(unused -> debugReporter.debug(() -> " " + unused));
-    ImmutableSet.Builder<String> resEntriesToKeep = new ImmutableSet.Builder<>();
+    ImmutableSet.Builder<String> resEntriesToKeepBuilder = new ImmutableSet.Builder<>();
     for (PathAndBytes xmlInput : Iterables.concat(xmlInputs, resFolderInputs)) {
       if (ResourceShrinkerImplKt.isJarPathReachable(resourceStore, xmlInput.path.toString())) {
-        resEntriesToKeep.add(xmlInput.path.toString());
+        resEntriesToKeepBuilder.add(xmlInput.path.toString());
+        if (xmlInput.duplicated) {
+          // Ensure that we don't remove references to duplicated res folder entries.
+          List<Resource> duplicatedResources =
+              ResourceShrinkerImplKt.getResourcesFor(resourceStore, xmlInput.path.toString());
+          unusedResources.removeAll(duplicatedResources);
+        }
       }
     }
+    debugReporter.debug(() -> "Unused resources are: ");
+    unusedResources.forEach(unused -> debugReporter.debug(() -> " " + unused));
     List<Integer> resourceIdsToRemove = getResourceIdsToRemove(unusedResources);
     Map<FeatureSplit, ResourceTable> shrunkenTables = new HashMap<>();
     for (Entry<PathAndBytes, FeatureSplit> entry : resourceTables.entrySet()) {
@@ -233,7 +239,7 @@
               ResourceTable.parseFrom(entry.getKey().bytes), resourceIdsToRemove);
       shrunkenTables.put(entry.getValue(), shrunkenResourceTable);
     }
-    return new ShrinkerResult(resEntriesToKeep.build(), shrunkenTables);
+    return new ShrinkerResult(resEntriesToKeepBuilder.build(), shrunkenTables);
   }
 
   private static List<Integer> getResourceIdsToRemove(List<Resource> unusedResources) {
@@ -331,12 +337,17 @@
   private static class PathAndBytes {
     private final byte[] bytes;
     private final String path;
+    private boolean duplicated;
 
     private PathAndBytes(byte[] bytes, String path) {
       this.bytes = bytes;
       this.path = path;
     }
 
+    public void setDuplicated(boolean duplicated) {
+      this.duplicated = duplicated;
+    }
+
     public String getPathWithoutRes() {
       assert path.startsWith("res/");
       return path.substring(4);
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 3dded16..4740a7c 100644
--- a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java
+++ b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java
@@ -54,6 +54,7 @@
   private final Function<Exception, RuntimeException> errorHandler;
   private final R8ResourceShrinkerModel r8ResourceShrinkerModel;
   private final Map<String, Supplier<InputStream>> xmlFileProviders = new HashMap<>();
+  private final Set<String> duplicatedResFolderEntries = new HashSet<>();
   private final List<Supplier<InputStream>> keepRuleFileProviders = new ArrayList<>();
 
   private final List<Supplier<InputStream>> manifestProviders = new ArrayList<>();
@@ -62,7 +63,7 @@
   private final ShrinkerDebugReporter shrinkerDebugReporter;
   private ClassReferenceCallback enqueuerCallback;
   private MethodReferenceCallback methodCallback;
-  private Map<Integer, List<String>> resourceIdToXmlFiles;
+  private Map<Integer, Set<String>> resourceIdToXmlFiles;
   private Set<String> packageNames;
   private final Set<String> seenNoneClassValues = new HashSet<>();
   private final Set<Integer> seenResourceIds = new HashSet<>();
@@ -172,7 +173,11 @@
   }
 
   public void addXmlFileProvider(Supplier<InputStream> inputStreamSupplier, String location) {
-    this.xmlFileProviders.put(location, inputStreamSupplier);
+    if (this.xmlFileProviders.containsKey(location)) {
+      duplicatedResFolderEntries.add(location);
+    } else {
+      this.xmlFileProviders.put(location, inputStreamSupplier);
+    }
   }
 
   public void addKeepRuleRileProvider(Supplier<InputStream> inputStreamSupplier) {
@@ -180,6 +185,9 @@
   }
 
   public void addResFileProvider(Supplier<InputStream> inputStreamSupplier, String location) {
+    if (this.resfileProviders.containsKey(location)) {
+      duplicatedResFolderEntries.add(location);
+    }
     this.resfileProviders.put(location, inputStreamSupplier);
   }
 
@@ -253,11 +261,24 @@
   }
 
   private void traceXmlForResourceId(int id) {
-    List<String> xmlFiles = getResourceIdToXmlFiles().get(id);
+    Set<String> xmlFiles = getResourceIdToXmlFiles().get(id);
     if (xmlFiles != null) {
       for (String xmlFile : xmlFiles) {
         InputStream inputStream = xmlFileProviders.get(xmlFile).get();
         traceXml(xmlFile, inputStream);
+        if (duplicatedResFolderEntries.contains(xmlFile)) {
+          traceDuplicatedXmlFileIds(id, xmlFile);
+        }
+      }
+    }
+  }
+
+  private void traceDuplicatedXmlFileIds(int currentId, String xmlFile) {
+    for (Map.Entry<Integer, Set<String>> entry : getResourceIdToXmlFiles().entrySet()) {
+      if (entry.getValue().contains(xmlFile)) {
+        if (entry.getKey() != currentId) {
+          trace(entry.getKey(), "Duplicated xmlfile " + xmlFile);
+        }
       }
     }
   }
@@ -339,7 +360,7 @@
     return packageName + "." + xmlAttribute.getValue();
   }
 
-  public Map<Integer, List<String>> getResourceIdToXmlFiles() {
+  public Map<Integer, Set<String>> getResourceIdToXmlFiles() {
     if (resourceIdToXmlFiles == null) {
       resourceIdToXmlFiles = new HashMap<>();
       for (ResourceTable resourceTable : resourceTables.values()) {
@@ -356,7 +377,7 @@
                       if (file.getType() == FileReference.Type.PROTO_XML) {
                         int id = ResourceTableUtilKt.toIdentifier(packageEntry, type, entry);
                         resourceIdToXmlFiles
-                            .computeIfAbsent(id, unused -> new ArrayList<>())
+                            .computeIfAbsent(id, unused -> new HashSet<>())
                             .add(file.getPath());
                       }
                     }
diff --git a/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkingWithFeaturesAndDuplicatedResEntryTest.java b/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkingWithFeaturesAndDuplicatedResEntryTest.java
new file mode 100644
index 0000000..ae4b83f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkingWithFeaturesAndDuplicatedResEntryTest.java
@@ -0,0 +1,144 @@
+// 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 com.android.tools.r8.R8FullTestBuilder;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResource;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResourceBuilder;
+import com.android.tools.r8.utils.BooleanUtils;
+import java.io.IOException;
+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 ResourceShrinkingWithFeaturesAndDuplicatedResEntryTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  private static final String SMALL_XML = "<Z/>";
+
+  @Parameter(1)
+  public boolean optimized;
+
+  @Parameters(name = "{0}, optimized: {1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withDefaultDexRuntime().withAllApiLevels().build(),
+        BooleanUtils.values());
+  }
+
+  public static AndroidTestResource getTestResources(TemporaryFolder temp) throws Exception {
+    return new AndroidTestResourceBuilder()
+        .withSimpleManifestAndAppNameString()
+        .addRClassInitializeWithDefaultValues(R.xml.class, R.string.class)
+        .addXml("base_used_xml.xml", SMALL_XML)
+        .addXml("base_unused_xml.xml", SMALL_XML)
+        .addXml("duplicated_xml.xml", SMALL_XML)
+        .build(temp);
+  }
+
+  public AndroidTestResource getFeatureSplitTestResources(TemporaryFolder temp) throws IOException {
+    return new AndroidTestResourceBuilder()
+        .withSimpleManifestAndAppNameString()
+        .setPackageId(0x7e)
+        .addRClassInitializeWithDefaultValues(FeatureSplit.R.xml.class, FeatureSplit.R.string.class)
+        .addXml("feature_used_xml.xml", SMALL_XML)
+        .addXml("feature_unused_xml.xml", SMALL_XML)
+        .addXml("duplicated_xml.xml", SMALL_XML)
+        .build(temp);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    TemporaryFolder featureSplitTemp = ToolHelper.getTemporaryFolderForTest();
+    featureSplitTemp.create();
+    testForR8(parameters.getBackend())
+        .setMinApi(parameters)
+        .addProgramClasses(Base.class)
+        .addFeatureSplit(FeatureSplit.FeatureSplitMain.class)
+        .addAndroidResources(getTestResources(temp))
+        .addFeatureSplitAndroidResources(
+            getFeatureSplitTestResources(featureSplitTemp), FeatureSplit.class.getName())
+        .applyIf(optimized, R8FullTestBuilder::enableOptimizedShrinking)
+        .addKeepMainRule(Base.class)
+        .addKeepMainRule(FeatureSplit.FeatureSplitMain.class)
+        .compile()
+        .inspectShrunkenResources(
+            resourceTableInspector -> {
+              resourceTableInspector.assertDoesNotContainResourceWithName("xml", "base_unused_xml");
+              resourceTableInspector.assertContainsResourceWithName("xml", "base_used_xml");
+              resourceTableInspector.assertContainsResourceWithName("xml", "duplicated_xml");
+              resourceTableInspector.assertDoesNotContainResourceWithName(
+                  "string", "duplicated_xml");
+            })
+        .inspectShrunkenResourcesForFeature(
+            resourceTableInspector -> {
+              resourceTableInspector.assertDoesNotContainResourceWithName(
+                  "xml", "feature_unused_xml");
+              resourceTableInspector.assertContainsResourceWithName("xml", "feature_used_xml");
+              resourceTableInspector.assertContainsResourceWithName("xml", "duplicated_xml");
+              resourceTableInspector.assertDoesNotContainResourceWithName(
+                  "string", "duplicated_xml");
+            },
+            FeatureSplit.class.getName())
+        .run(parameters.getRuntime(), Base.class)
+        .assertSuccess();
+  }
+
+  public static class Base {
+
+    public static void main(String[] args) {
+      if (System.currentTimeMillis() == 0) {
+        System.out.println(R.xml.base_used_xml);
+        System.out.println(R.xml.duplicated_xml);
+      }
+    }
+  }
+
+  public static class R {
+    public static class string {
+      // We should still remove unreachable entries with overlapping name if the type is different
+      public static int duplicated_xml;
+    }
+
+    public static class xml {
+      public static int base_used_xml;
+      public static int base_unused_xml;
+      public static int duplicated_xml;
+    }
+  }
+
+  public static class FeatureSplit {
+    public static class FeatureSplitMain {
+      public static void main(String[] args) {
+        if (System.currentTimeMillis() == 0) {
+          System.out.println(R.xml.feature_used_xml);
+        }
+      }
+    }
+
+    public static class R {
+      public static class string {
+        // We should still remove unreachable entries with overlapping name if the type is different
+        public static int duplicated_xml;
+      }
+
+      public static class xml {
+        public static int feature_used_xml;
+        public static int feature_unused_xml;
+        // Unused in the feature
+        public static int duplicated_xml;
+      }
+    }
+  }
+}