Augment ResourceProcessorBusybox with capability to auto-resolve unambiguous attr resource conflicts.

When a merge conflict has been detected between a strong attr resource and a weak attr resource which doesn't specify format, this is acceptable. Select the strong attr and keep the conflict from being reported as a conflict needing manual resolution.

RELNOTES: Android resource conflicts will no longer be reported between a strong attr resource and a weak attr resource, if the weak attr does not have format specified.
PiperOrigin-RevId: 243344206
diff --git a/src/test/java/com/google/devtools/build/android/AttributeResourcesAndroidDataMergerTest.java b/src/test/java/com/google/devtools/build/android/AttributeResourcesAndroidDataMergerTest.java
new file mode 100644
index 0000000..a306e06
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/AttributeResourcesAndroidDataMergerTest.java
@@ -0,0 +1,536 @@
+// Copyright 2019 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android;
+
+import static com.google.common.truth.Truth.assertAbout;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.devtools.build.android.ParsedAndroidDataBuilder.xml;
+
+import com.android.aapt.Resources.Attribute;
+import com.android.aapt.Resources.CompoundValue;
+import com.android.aapt.Resources.Value;
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.jimfs.Jimfs;
+import com.google.common.truth.Subject;
+import com.google.devtools.build.android.xml.AttrXmlResourceValue;
+import com.google.devtools.build.android.xml.AttrXmlResourceValue.ReferenceResourceXmlAttrValue;
+import com.google.devtools.build.android.xml.AttrXmlResourceValue.StringResourceXmlAttrValue;
+import java.nio.file.FileSystem;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+/** Parameterized tests for {@link AndroidDataMerger} focusing on merging attribute resoutces. */
+@RunWith(Parameterized.class)
+public final class AttributeResourcesAndroidDataMergerTest {
+
+  /** For test data readability, represent strength as a 2-state enum rather than a boolean. */
+  enum Strength {
+    STRONG,
+    WEAK
+  }
+
+  @Parameters(name = "{index}: {0}")
+  public static Collection<Object[]> data() {
+    return Arrays.asList(
+        new Object[][] {
+          {
+            TestParameters.builder()
+                .set1(Strength.STRONG, 1)
+                .set2(Strength.STRONG, 0xFFFF)
+                .setExpectedMergeConflict(
+                    ctx ->
+                        MergeConflict.of(
+                            ctx.fqnFactory.parse("attr/ambiguousName"),
+                            DataResourceXml.createWithNoNamespace(
+                                ctx.transitiveRoot1.resolve("res/values/attrs.xml"),
+                                AttrXmlResourceValue.fromFormatEntries(
+                                    ReferenceResourceXmlAttrValue.asEntry())),
+                            DataResourceXml.createWithNoNamespace(
+                                ctx.transitiveRoot2.resolve("res/values/attrs.xml"),
+                                AttrXmlResourceValue.fromFormatEntries())))
+                .build()
+          },
+          {
+            TestParameters.builder()
+                .set1(Strength.WEAK, 1)
+                .set2(Strength.WEAK, 0xFFFF)
+                .setExpectedMergeConflict(
+                    ctx ->
+                        MergeConflict.of(
+                            ctx.fqnFactory.parse("attr/ambiguousName"),
+                            DataResourceXml.createWithNoNamespace(
+                                ctx.transitiveRoot1.resolve("res/values/attrs.xml"),
+                                AttrXmlResourceValue.weakFromFormatEntries(
+                                    ReferenceResourceXmlAttrValue.asEntry())),
+                            DataResourceXml.createWithNoNamespace(
+                                ctx.transitiveRoot2.resolve("res/values/attrs.xml"),
+                                AttrXmlResourceValue.weakFromFormatEntries())))
+                .build()
+          },
+          {
+            TestParameters.builder()
+                .set1(Strength.STRONG, 1)
+                .set2(Strength.STRONG, 1)
+                .setExpectedMergedAndroidData(
+                    ctx ->
+                        UnwrittenMergedAndroidData.of(
+                            ctx.primary.getManifest(),
+                            ParsedAndroidDataBuilder.empty(),
+                            ParsedAndroidDataBuilder.buildOn(ctx.fqnFactory)
+                                .overwritable(
+                                    xml("attr/ambiguousName")
+                                        .root(ctx.primaryRoot)
+                                        .source(ctx.transitiveAttr2.overwrite(ctx.transitiveAttr1))
+                                        .value(
+                                            AttrXmlResourceValue.fromFormatEntries(
+                                                ReferenceResourceXmlAttrValue.asEntry())))
+                                .build()))
+                .build()
+          },
+          {
+            TestParameters.builder()
+                .set1(Strength.WEAK, 1)
+                .set2(Strength.WEAK, 1)
+                .setExpectedMergedAndroidData(
+                    ctx ->
+                        UnwrittenMergedAndroidData.of(
+                            ctx.primary.getManifest(),
+                            ParsedAndroidDataBuilder.empty(),
+                            ParsedAndroidDataBuilder.buildOn(ctx.fqnFactory)
+                                .overwritable(
+                                    xml("attr/ambiguousName")
+                                        .root(ctx.primaryRoot)
+                                        .source(ctx.transitiveAttr2.overwrite(ctx.transitiveAttr1))
+                                        .value(
+                                            AttrXmlResourceValue.weakFromFormatEntries(
+                                                ReferenceResourceXmlAttrValue.asEntry())))
+                                .build()))
+                .build()
+          },
+          {
+            TestParameters.builder()
+                .set1(Strength.STRONG, 1)
+                .set2(Strength.WEAK, 0xFFFF)
+                .setExpectedMergedAndroidData(
+                    ctx ->
+                        UnwrittenMergedAndroidData.of(
+                            ctx.primary.getManifest(),
+                            ParsedAndroidDataBuilder.empty(),
+                            ParsedAndroidDataBuilder.buildOn(ctx.fqnFactory)
+                                .overwritable(
+                                    xml("attr/ambiguousName")
+                                        .root(ctx.primaryRoot)
+                                        .source(ctx.transitiveAttr1.overwrite(ctx.transitiveAttr2))
+                                        .value(
+                                            AttrXmlResourceValue.fromFormatEntries(
+                                                ReferenceResourceXmlAttrValue.asEntry())))
+                                .build()))
+                .build()
+          },
+          {
+            TestParameters.builder()
+                .set1(Strength.WEAK, 0xFFFF)
+                .set2(Strength.STRONG, 2)
+                .setExpectedMergedAndroidData(
+                    ctx ->
+                        UnwrittenMergedAndroidData.of(
+                            ctx.primary.getManifest(),
+                            ParsedAndroidDataBuilder.empty(),
+                            ParsedAndroidDataBuilder.buildOn(ctx.fqnFactory)
+                                .overwritable(
+                                    xml("attr/ambiguousName")
+                                        .root(ctx.primaryRoot)
+                                        .source(ctx.transitiveAttr2.overwrite(ctx.transitiveAttr1))
+                                        .value(
+                                            AttrXmlResourceValue.fromFormatEntries(
+                                                StringResourceXmlAttrValue.asEntry())))
+                                .build()))
+                .build()
+          },
+          {
+            TestParameters.builder()
+                .set1(Strength.STRONG, 1)
+                .set2(Strength.WEAK, 2)
+                .setExpectedMergeConflict(
+                    ctx ->
+                        MergeConflict.of(
+                            ctx.fqnFactory.parse("attr/ambiguousName"),
+                            DataResourceXml.createWithNoNamespace(
+                                ctx.transitiveRoot1.resolve("res/values/attrs.xml"),
+                                AttrXmlResourceValue.fromFormatEntries(
+                                    ReferenceResourceXmlAttrValue.asEntry())),
+                            DataResourceXml.createWithNoNamespace(
+                                ctx.transitiveRoot2.resolve("res/values/attrs.xml"),
+                                AttrXmlResourceValue.weakFromFormatEntries(
+                                    StringResourceXmlAttrValue.asEntry()))))
+                .build()
+          },
+          {
+            TestParameters.builder()
+                .set1(Strength.WEAK, 1)
+                .set2(Strength.STRONG, 2)
+                .setExpectedMergeConflict(
+                    ctx ->
+                        MergeConflict.of(
+                            ctx.fqnFactory.parse("attr/ambiguousName"),
+                            DataResourceXml.createWithNoNamespace(
+                                ctx.transitiveRoot1.resolve("res/values/attrs.xml"),
+                                AttrXmlResourceValue.weakFromFormatEntries(
+                                    ReferenceResourceXmlAttrValue.asEntry())),
+                            DataResourceXml.createWithNoNamespace(
+                                ctx.transitiveRoot2.resolve("res/values/attrs.xml"),
+                                AttrXmlResourceValue.fromFormatEntries(
+                                    StringResourceXmlAttrValue.asEntry()))))
+                .build()
+          },
+          {
+            TestParameters.builder()
+                .set1(Strength.STRONG, 1)
+                .set2(Strength.WEAK, 1)
+                .setExpectedMergedAndroidData(
+                    ctx ->
+                        UnwrittenMergedAndroidData.of(
+                            ctx.primary.getManifest(),
+                            ParsedAndroidDataBuilder.empty(),
+                            ParsedAndroidDataBuilder.buildOn(ctx.fqnFactory)
+                                .overwritable(
+                                    xml("attr/ambiguousName")
+                                        .root(ctx.primaryRoot)
+                                        .source(ctx.transitiveAttr1.overwrite(ctx.transitiveAttr2))
+                                        .value(
+                                            AttrXmlResourceValue.fromFormatEntries(
+                                                ReferenceResourceXmlAttrValue.asEntry())))
+                                .build()))
+                .build()
+          },
+          {
+            TestParameters.builder()
+                .set1(Strength.WEAK, 1)
+                .set2(Strength.STRONG, 1)
+                .setExpectedMergedAndroidData(
+                    ctx ->
+                        UnwrittenMergedAndroidData.of(
+                            ctx.primary.getManifest(),
+                            ParsedAndroidDataBuilder.empty(),
+                            ParsedAndroidDataBuilder.buildOn(ctx.fqnFactory)
+                                .overwritable(
+                                    xml("attr/ambiguousName")
+                                        .root(ctx.primaryRoot)
+                                        .source(ctx.transitiveAttr2.overwrite(ctx.transitiveAttr1))
+                                        .value(
+                                            AttrXmlResourceValue.fromFormatEntries(
+                                                ReferenceResourceXmlAttrValue.asEntry())))
+                                .build()))
+                .build()
+          },
+          {
+            TestParameters.builder()
+                .set1(Strength.STRONG, 0xFFFF)
+                .set2(Strength.STRONG, 0xFFFF)
+                .setExpectedMergedAndroidData(
+                    ctx ->
+                        UnwrittenMergedAndroidData.of(
+                            ctx.primary.getManifest(),
+                            ParsedAndroidDataBuilder.empty(),
+                            ParsedAndroidDataBuilder.buildOn(ctx.fqnFactory)
+                                .overwritable(
+                                    xml("attr/ambiguousName")
+                                        .root(ctx.primaryRoot)
+                                        .source(ctx.transitiveAttr2.overwrite(ctx.transitiveAttr1))
+                                        .value(AttrXmlResourceValue.fromFormatEntries()))
+                                .build()))
+                .build()
+          },
+          {
+            TestParameters.builder()
+                .set1(Strength.WEAK, 0xFFFF)
+                .set2(Strength.WEAK, 0xFFFF)
+                .setExpectedMergedAndroidData(
+                    ctx ->
+                        UnwrittenMergedAndroidData.of(
+                            ctx.primary.getManifest(),
+                            ParsedAndroidDataBuilder.empty(),
+                            ParsedAndroidDataBuilder.buildOn(ctx.fqnFactory)
+                                .overwritable(
+                                    xml("attr/ambiguousName")
+                                        .root(ctx.primaryRoot)
+                                        .source(ctx.transitiveAttr2.overwrite(ctx.transitiveAttr1))
+                                        .value(AttrXmlResourceValue.weakFromFormatEntries()))
+                                .build()))
+                .build()
+          },
+          {
+            TestParameters.builder()
+                .set1(Strength.STRONG, 0xFFFF)
+                .set2(Strength.WEAK, 0xFFFF)
+                .setExpectedMergedAndroidData(
+                    ctx ->
+                        UnwrittenMergedAndroidData.of(
+                            ctx.primary.getManifest(),
+                            ParsedAndroidDataBuilder.empty(),
+                            ParsedAndroidDataBuilder.buildOn(ctx.fqnFactory)
+                                .overwritable(
+                                    xml("attr/ambiguousName")
+                                        .root(ctx.primaryRoot)
+                                        .source(ctx.transitiveAttr1.overwrite(ctx.transitiveAttr2))
+                                        .value(AttrXmlResourceValue.fromFormatEntries()))
+                                .build()))
+                .build()
+          },
+          {
+            TestParameters.builder()
+                .set1(Strength.WEAK, 0xFFFF)
+                .set2(Strength.STRONG, 0xFFFF)
+                .setExpectedMergedAndroidData(
+                    ctx ->
+                        UnwrittenMergedAndroidData.of(
+                            ctx.primary.getManifest(),
+                            ParsedAndroidDataBuilder.empty(),
+                            ParsedAndroidDataBuilder.buildOn(ctx.fqnFactory)
+                                .overwritable(
+                                    xml("attr/ambiguousName")
+                                        .root(ctx.primaryRoot)
+                                        .source(ctx.transitiveAttr2.overwrite(ctx.transitiveAttr1))
+                                        .value(AttrXmlResourceValue.fromFormatEntries()))
+                                .build()))
+                .build()
+          },
+          {
+            TestParameters.builder()
+                .set1(Strength.STRONG, 0xFFFF)
+                .set2(Strength.WEAK, 1)
+                .setExpectedMergeConflict(
+                    ctx ->
+                        MergeConflict.of(
+                            ctx.fqnFactory.parse("attr/ambiguousName"),
+                            DataResourceXml.createWithNoNamespace(
+                                ctx.transitiveRoot1.resolve("res/values/attrs.xml"),
+                                AttrXmlResourceValue.fromFormatEntries()),
+                            DataResourceXml.createWithNoNamespace(
+                                ctx.transitiveRoot2.resolve("res/values/attrs.xml"),
+                                AttrXmlResourceValue.weakFromFormatEntries(
+                                    ReferenceResourceXmlAttrValue.asEntry()))))
+                .build()
+          },
+          {
+            TestParameters.builder()
+                .set1(Strength.WEAK, 1)
+                .set2(Strength.STRONG, 0xFFFF)
+                .setExpectedMergeConflict(
+                    ctx ->
+                        MergeConflict.of(
+                            ctx.fqnFactory.parse("attr/ambiguousName"),
+                            DataResourceXml.createWithNoNamespace(
+                                ctx.transitiveRoot1.resolve("res/values/attrs.xml"),
+                                AttrXmlResourceValue.weakFromFormatEntries(
+                                    ReferenceResourceXmlAttrValue.asEntry())),
+                            DataResourceXml.createWithNoNamespace(
+                                ctx.transitiveRoot2.resolve("res/values/attrs.xml"),
+                                AttrXmlResourceValue.fromFormatEntries())))
+                .build()
+          }
+        });
+  }
+
+  @Parameter public TestParameters testParameters;
+
+  private FullyQualifiedName.Factory fqnFactory;
+  private TestLoggingHandler loggingHandler;
+  private Logger mergerLogger;
+  private Path primaryRoot;
+  private Path transitiveRoot1;
+  private Path transitiveRoot2;
+  private DataSource transitiveAttr1;
+  private DataSource transitiveAttr2;
+  private UnvalidatedAndroidData primary;
+
+  @Before
+  public void setUp() throws Exception {
+    FileSystem fileSystem = Jimfs.newFileSystem();
+    fqnFactory = FullyQualifiedName.Factory.from(ImmutableList.of());
+    mergerLogger = Logger.getLogger(AndroidDataMerger.class.getCanonicalName());
+    loggingHandler = new TestLoggingHandler();
+    mergerLogger.addHandler(loggingHandler);
+    primaryRoot = fileSystem.getPath("primary");
+    transitiveRoot1 = fileSystem.getPath("transitive1");
+    transitiveRoot2 = fileSystem.getPath("transitive2");
+    transitiveAttr1 = DataSource.of(transitiveRoot1.resolve("res").resolve("values/attrs.xml"));
+    transitiveAttr2 = DataSource.of(transitiveRoot2.resolve("res").resolve("values/attrs.xml"));
+    primary =
+        AndroidDataBuilder.of(primaryRoot)
+            .createManifest("AndroidManifest.xml", "com.google.mergetest")
+            .buildUnvalidated();
+  }
+
+  @After
+  public void removeLoggingHandler() {
+    mergerLogger.removeHandler(loggingHandler);
+  }
+
+  @Test
+  public void test() throws Exception {
+    ParsedAndroidData transitiveDependency =
+        ParsedAndroidDataBuilder.buildOn(fqnFactory)
+            .overwritable(
+                xml("attr/ambiguousName")
+                    .root(transitiveRoot1)
+                    .source("values/attrs.xml")
+                    .value(
+                        AttrXmlResourceValue.from(
+                            Value.newBuilder()
+                                .setCompoundValue(
+                                    CompoundValue.newBuilder()
+                                        .setAttr(
+                                            Attribute.newBuilder()
+                                                .setFormatFlags(testParameters.formatFlags1())))
+                                .setWeak(testParameters.strength1() == Strength.WEAK)
+                                .build())))
+            .overwritable(
+                xml("attr/ambiguousName")
+                    .root(transitiveRoot2)
+                    .source("values/attrs.xml")
+                    .value(
+                        AttrXmlResourceValue.from(
+                            Value.newBuilder()
+                                .setCompoundValue(
+                                    CompoundValue.newBuilder()
+                                        .setAttr(
+                                            Attribute.newBuilder()
+                                                .setFormatFlags(testParameters.formatFlags2())))
+                                .setWeak(testParameters.strength2() == Strength.WEAK)
+                                .build())))
+            .build();
+
+    ParsedAndroidData directDependency = ParsedAndroidDataBuilder.empty();
+
+    AndroidDataMerger merger = AndroidDataMerger.createWithDefaults();
+
+    UnwrittenMergedAndroidData data =
+        merger.merge(transitiveDependency, directDependency, primary, false, false);
+
+    if (testParameters.expectedMergedAndroidData().isPresent()) {
+      assertAbout(unwrittenMergedAndroidData)
+          .that(data)
+          .isEqualTo(testParameters.expectedMergedAndroidData().get().apply(this));
+    }
+
+    if (testParameters.expectedMergeConflict().isPresent()) {
+      assertThat(loggingHandler.warnings)
+          .containsExactly(
+              testParameters.expectedMergeConflict().get().apply(this).toConflictMessage());
+    } else {
+      assertThat(loggingHandler.warnings).isEmpty();
+    }
+  }
+
+  private final Subject.Factory<UnwrittenMergedAndroidDataSubject, UnwrittenMergedAndroidData>
+      unwrittenMergedAndroidData = UnwrittenMergedAndroidDataSubject::new;
+
+  private static final class TestLoggingHandler extends Handler {
+    public final List<String> warnings = new ArrayList<>();
+
+    @Override
+    public void publish(LogRecord record) {
+      if (record.getLevel().equals(Level.WARNING)) {
+        warnings.add(record.getMessage());
+      }
+    }
+
+    @Override
+    public void flush() {}
+
+    @Override
+    public void close() {}
+  }
+
+  @AutoValue
+  abstract static class TestParameters {
+    abstract Strength strength1();
+
+    abstract int formatFlags1();
+
+    abstract Strength strength2();
+
+    abstract int formatFlags2();
+
+    abstract Optional<Function<AttributeResourcesAndroidDataMergerTest, UnwrittenMergedAndroidData>>
+        expectedMergedAndroidData();
+
+    abstract Optional<Function<AttributeResourcesAndroidDataMergerTest, MergeConflict>>
+        expectedMergeConflict();
+
+    @Override
+    public final String toString() {
+      return Joiner.on(", ")
+          .join(
+              strength1(),
+              formatFlags1(),
+              strength2(),
+              formatFlags2(),
+              expectedMergeConflict().isPresent()
+                  ? "conflict expected"
+                  : "successful merge expected");
+    }
+
+    static Builder builder() {
+      return new AutoValue_AttributeResourcesAndroidDataMergerTest_TestParameters.Builder();
+    }
+
+    @AutoValue.Builder
+    abstract static class Builder {
+
+      Builder set1(Strength strength, int formatFlags) {
+        return setStrength1(strength).setFormatFlags1(formatFlags);
+      }
+
+      Builder set2(Strength strength, int formatFlags) {
+        return setStrength2(strength).setFormatFlags2(formatFlags);
+      }
+
+      abstract Builder setStrength1(Strength value);
+
+      abstract Builder setFormatFlags1(int value);
+
+      abstract Builder setStrength2(Strength value);
+
+      abstract Builder setFormatFlags2(int value);
+
+      abstract Builder setExpectedMergedAndroidData(
+          Function<AttributeResourcesAndroidDataMergerTest, UnwrittenMergedAndroidData> value);
+
+      abstract Builder setExpectedMergeConflict(
+          Function<AttributeResourcesAndroidDataMergerTest, MergeConflict> value);
+
+      abstract TestParameters build();
+    }
+  }
+}
diff --git a/src/tools/android/java/com/google/devtools/build/android/DataResourceXml.java b/src/tools/android/java/com/google/devtools/build/android/DataResourceXml.java
index 39aec42..ae0322c 100644
--- a/src/tools/android/java/com/google/devtools/build/android/DataResourceXml.java
+++ b/src/tools/android/java/com/google/devtools/build/android/DataResourceXml.java
@@ -432,4 +432,17 @@
     DataResourceXml other = (DataResourceXml) value;
     return Objects.equals(xml, other.xml);
   }
+
+  @Override
+  public int compareMergePriorityTo(DataValue value) {
+    Preconditions.checkNotNull(value);
+    if (!(value instanceof DataResourceXml)) {
+      throw new IllegalArgumentException(
+          String.format(
+              "Can only compare priority with another %s, but was given a %s",
+              DataResourceXml.class.getSimpleName(), value.getClass().getSimpleName()));
+    }
+    DataResourceXml other = (DataResourceXml) value;
+    return xml.compareMergePriorityTo(other.xml);
+  }
 }
diff --git a/src/tools/android/java/com/google/devtools/build/android/DataValue.java b/src/tools/android/java/com/google/devtools/build/android/DataValue.java
index de95745..6c327b7 100644
--- a/src/tools/android/java/com/google/devtools/build/android/DataValue.java
+++ b/src/tools/android/java/com/google/devtools/build/android/DataValue.java
@@ -42,4 +42,11 @@
    * equivalent to another given DataValue object
    */
   boolean valueEquals(DataValue value);
+
+  /**
+   * Compares priority in hopes of auto-resolving a merge conflict. Returns 1 if the value
+   * properties are higher in priority than those in another given DataValue object, -1 if lower
+   * priority, 0 if same priority (i.e. in conflict).
+   */
+  int compareMergePriorityTo(DataValue value);
 }
diff --git a/src/tools/android/java/com/google/devtools/build/android/DataValueFile.java b/src/tools/android/java/com/google/devtools/build/android/DataValueFile.java
index 3270e3c..140f343 100644
--- a/src/tools/android/java/com/google/devtools/build/android/DataValueFile.java
+++ b/src/tools/android/java/com/google/devtools/build/android/DataValueFile.java
@@ -134,4 +134,9 @@
   public boolean valueEquals(DataValue value) {
     return equals(value);
   }
+
+  @Override
+  public int compareMergePriorityTo(DataValue value) {
+    return 0;
+  }
 }
diff --git a/src/tools/android/java/com/google/devtools/build/android/MergeConflict.java b/src/tools/android/java/com/google/devtools/build/android/MergeConflict.java
index 5a98086..d1077f0 100644
--- a/src/tools/android/java/com/google/devtools/build/android/MergeConflict.java
+++ b/src/tools/android/java/com/google/devtools/build/android/MergeConflict.java
@@ -88,6 +88,7 @@
 
   boolean isValidWith(SourceChecker checker) throws IOException {
     return !primary.valueEquals(overwritten)
+        && primary.compareMergePriorityTo(overwritten) == 0
         && !checker.checkEquality(primary.source(), overwritten.source());
   }
 
diff --git a/src/tools/android/java/com/google/devtools/build/android/ParsedAndroidData.java b/src/tools/android/java/com/google/devtools/build/android/ParsedAndroidData.java
index 9d7380e..5252767 100644
--- a/src/tools/android/java/com/google/devtools/build/android/ParsedAndroidData.java
+++ b/src/tools/android/java/com/google/devtools/build/android/ParsedAndroidData.java
@@ -193,7 +193,11 @@
         } else if (value.source().hasOveridden(other.source())) {
           target.put(key, value);
         } else {
-          target.put(key, overwrite(key, value, other));
+          target.put(
+              key,
+              value.compareMergePriorityTo(other) >= 0
+                  ? overwrite(key, value, other)
+                  : overwrite(key, other, value));
         }
       } else {
         target.put(key, value);
diff --git a/src/tools/android/java/com/google/devtools/build/android/XmlResourceValue.java b/src/tools/android/java/com/google/devtools/build/android/XmlResourceValue.java
index f24d6b2..49e38c3 100644
--- a/src/tools/android/java/com/google/devtools/build/android/XmlResourceValue.java
+++ b/src/tools/android/java/com/google/devtools/build/android/XmlResourceValue.java
@@ -41,6 +41,12 @@
   XmlResourceValue combineWith(XmlResourceValue value);
 
   /**
+   * Returns 1 if the xml value is higher priority than the given value, -1 if lower priority, and 0
+   * if equal priority.
+   */
+  int compareMergePriorityTo(XmlResourceValue value);
+
+  /**
    * Queue up writing the resource to the given {@link AndroidResourceClassWriter}. Each resource
    * can generate one or more (in the case of styleable) fields and inner classes in the R class.
    *
diff --git a/src/tools/android/java/com/google/devtools/build/android/xml/ArrayXmlResourceValue.java b/src/tools/android/java/com/google/devtools/build/android/xml/ArrayXmlResourceValue.java
index fb74fd2..7be5681 100644
--- a/src/tools/android/java/com/google/devtools/build/android/xml/ArrayXmlResourceValue.java
+++ b/src/tools/android/java/com/google/devtools/build/android/xml/ArrayXmlResourceValue.java
@@ -210,6 +210,11 @@
   }
 
   @Override
+  public int compareMergePriorityTo(XmlResourceValue value) {
+    return 0;
+  }
+
+  @Override
   public void writeResourceToClass(FullyQualifiedName key, AndroidResourceSymbolSink sink) {
     sink.acceptSimpleResource(key.type(), key.name());
   }
diff --git a/src/tools/android/java/com/google/devtools/build/android/xml/AttrXmlResourceValue.java b/src/tools/android/java/com/google/devtools/build/android/xml/AttrXmlResourceValue.java
index e7f9e09..6c924de 100644
--- a/src/tools/android/java/com/google/devtools/build/android/xml/AttrXmlResourceValue.java
+++ b/src/tools/android/java/com/google/devtools/build/android/xml/AttrXmlResourceValue.java
@@ -90,9 +90,11 @@
   private static final QName TAG_ENUM = QName.valueOf(ENUM);
   private static final QName TAG_FLAG = QName.valueOf("flag");
   private final ImmutableMap<String, ResourceXmlAttrValue> formats;
+  private final boolean weak;
 
-  private AttrXmlResourceValue(ImmutableMap<String, ResourceXmlAttrValue> formats) {
+  private AttrXmlResourceValue(ImmutableMap<String, ResourceXmlAttrValue> formats, boolean weak) {
     this.formats = formats;
+    this.weak = weak;
   }
 
   private static Map<String, String> readSubValues(XMLEventReader reader, QName subTagType)
@@ -119,7 +121,6 @@
     }
   }
 
-  @VisibleForTesting
   private static final class BuilderEntry implements Map.Entry<String, ResourceXmlAttrValue> {
     private final String name;
     private final ResourceXmlAttrValue value;
@@ -152,6 +153,13 @@
     return of(ImmutableMap.copyOf(Arrays.asList(entries)));
   }
 
+  @SafeVarargs
+  @VisibleForTesting
+  public static XmlResourceValue weakFromFormatEntries(
+      Map.Entry<String, ResourceXmlAttrValue>... entries) {
+    return of(ImmutableMap.copyOf(Arrays.asList(entries)), true);
+  }
+
   @SuppressWarnings("deprecation")
   public static XmlResourceValue from(SerializeFormat.DataValueXml proto)
       throws InvalidProtocolBufferException {
@@ -254,7 +262,7 @@
         throw new InvalidProtocolBufferException("Unexpected format flags: " + formatFlags);
       }
     }
-    return of(formats.build());
+    return of(formats.build(), proto.getWeak());
   }
 
   /** Creates a new {@link AttrXmlResourceValue}. Returns null if there are no formats. */
@@ -322,7 +330,12 @@
   }
 
   public static XmlResourceValue of(ImmutableMap<String, ResourceXmlAttrValue> formats) {
-    return new AttrXmlResourceValue(formats);
+    return new AttrXmlResourceValue(formats, /* weak= */ false);
+  }
+
+  public static XmlResourceValue of(
+      ImmutableMap<String, ResourceXmlAttrValue> formats, boolean weak) {
+    return new AttrXmlResourceValue(formats, weak);
   }
 
   @Override
@@ -334,17 +347,17 @@
       return false;
     }
     AttrXmlResourceValue other = (AttrXmlResourceValue) o;
-    return Objects.equals(formats, other.formats);
+    return Objects.equals(formats, other.formats) && weak == other.weak;
   }
 
   @Override
   public int hashCode() {
-    return formats.hashCode();
+    return Objects.hash(formats, weak);
   }
 
   @Override
   public String toString() {
-    return MoreObjects.toStringHelper(this).add("formats", formats).toString();
+    return MoreObjects.toStringHelper(this).add("formats", formats).add("weak", weak).toString();
   }
 
   @Override
@@ -424,6 +437,23 @@
     throw new IllegalArgumentException(this + " is not a combinable resource.");
   }
 
+  @Override
+  public int compareMergePriorityTo(XmlResourceValue value) {
+    if (!(value instanceof AttrXmlResourceValue)) {
+      throw new IllegalArgumentException(
+          String.format(
+              "Can only compare priority with another %s, but was given a %s",
+              AttrXmlResourceValue.class.getSimpleName(), value.getClass().getSimpleName()));
+    }
+    AttrXmlResourceValue that = (AttrXmlResourceValue) value;
+    if (!weak && that.weak && (that.formats.isEmpty() || formats.equals(that.formats))) {
+      return 1;
+    } else if (!that.weak && weak && (formats.isEmpty() || formats.equals(that.formats))) {
+      return -1;
+    }
+    return 0;
+  }
+
   /** Represents the xml value for an attr definition. */
   @CheckReturnValue
   public interface ResourceXmlAttrValue {
@@ -812,6 +842,7 @@
   @Override
   public String asConflictStringWith(DataSource source) {
     return String.format(
-        "%s [format(s): %s]", source.asConflictString(), String.join("|", this.formats.keySet()));
+        "%s [format(s): %s], [weak: %s]",
+        source.asConflictString(), String.join("|", this.formats.keySet()), weak);
   }
 }
diff --git a/src/tools/android/java/com/google/devtools/build/android/xml/IdXmlResourceValue.java b/src/tools/android/java/com/google/devtools/build/android/xml/IdXmlResourceValue.java
index 7c61feb..0f2a42c 100644
--- a/src/tools/android/java/com/google/devtools/build/android/xml/IdXmlResourceValue.java
+++ b/src/tools/android/java/com/google/devtools/build/android/xml/IdXmlResourceValue.java
@@ -147,6 +147,11 @@
   }
 
   @Override
+  public int compareMergePriorityTo(XmlResourceValue value) {
+    return 0;
+  }
+
+  @Override
   public String asConflictStringWith(DataSource source) {
     return source.asConflictString();
   }
diff --git a/src/tools/android/java/com/google/devtools/build/android/xml/PluralXmlResourceValue.java b/src/tools/android/java/com/google/devtools/build/android/xml/PluralXmlResourceValue.java
index f26aa0c..5668d9b 100644
--- a/src/tools/android/java/com/google/devtools/build/android/xml/PluralXmlResourceValue.java
+++ b/src/tools/android/java/com/google/devtools/build/android/xml/PluralXmlResourceValue.java
@@ -179,7 +179,12 @@
   public XmlResourceValue combineWith(XmlResourceValue value) {
     throw new IllegalArgumentException(this + " is not a combinable resource.");
   }
-  
+
+  @Override
+  public int compareMergePriorityTo(XmlResourceValue value) {
+    return 0;
+  }
+
   @Override
   public String asConflictStringWith(DataSource source) {
     return source.asConflictString();
diff --git a/src/tools/android/java/com/google/devtools/build/android/xml/PublicXmlResourceValue.java b/src/tools/android/java/com/google/devtools/build/android/xml/PublicXmlResourceValue.java
index 826a9b1..c51636f 100644
--- a/src/tools/android/java/com/google/devtools/build/android/xml/PublicXmlResourceValue.java
+++ b/src/tools/android/java/com/google/devtools/build/android/xml/PublicXmlResourceValue.java
@@ -177,7 +177,12 @@
     }
     return of(combined);
   }
-  
+
+  @Override
+  public int compareMergePriorityTo(XmlResourceValue value) {
+    return 0;
+  }
+
   @Override
   public String asConflictStringWith(DataSource source) {
     return source.asConflictString();
diff --git a/src/tools/android/java/com/google/devtools/build/android/xml/ResourcesAttribute.java b/src/tools/android/java/com/google/devtools/build/android/xml/ResourcesAttribute.java
index 2b88826..6cc08b9 100644
--- a/src/tools/android/java/com/google/devtools/build/android/xml/ResourcesAttribute.java
+++ b/src/tools/android/java/com/google/devtools/build/android/xml/ResourcesAttribute.java
@@ -171,6 +171,11 @@
   }
 
   @Override
+  public int compareMergePriorityTo(XmlResourceValue value) {
+    return 0;
+  }
+
+  @Override
   public void writeResourceToClass(FullyQualifiedName key, AndroidResourceSymbolSink sink) {
     // This is an xml attribute and does not have any java representation.
   }
diff --git a/src/tools/android/java/com/google/devtools/build/android/xml/SimpleXmlResourceValue.java b/src/tools/android/java/com/google/devtools/build/android/xml/SimpleXmlResourceValue.java
index 16de261..0acfa0e 100644
--- a/src/tools/android/java/com/google/devtools/build/android/xml/SimpleXmlResourceValue.java
+++ b/src/tools/android/java/com/google/devtools/build/android/xml/SimpleXmlResourceValue.java
@@ -268,6 +268,11 @@
   }
 
   @Override
+  public int compareMergePriorityTo(XmlResourceValue value) {
+    return 0;
+  }
+
+  @Override
   public String asConflictStringWith(DataSource source) {
     if (value != null) {
       return String.format(" %s (with value %s)", source.asConflictString(), value);
diff --git a/src/tools/android/java/com/google/devtools/build/android/xml/StyleXmlResourceValue.java b/src/tools/android/java/com/google/devtools/build/android/xml/StyleXmlResourceValue.java
index 4ec0454..28fb3a0 100644
--- a/src/tools/android/java/com/google/devtools/build/android/xml/StyleXmlResourceValue.java
+++ b/src/tools/android/java/com/google/devtools/build/android/xml/StyleXmlResourceValue.java
@@ -194,7 +194,12 @@
   public XmlResourceValue combineWith(XmlResourceValue value) {
     throw new IllegalArgumentException(this + " is not a combinable resource.");
   }
-  
+
+  @Override
+  public int compareMergePriorityTo(XmlResourceValue value) {
+    return 0;
+  }
+
   @Override
   public String asConflictStringWith(DataSource source) {
     return source.asConflictString();
diff --git a/src/tools/android/java/com/google/devtools/build/android/xml/StyleableXmlResourceValue.java b/src/tools/android/java/com/google/devtools/build/android/xml/StyleableXmlResourceValue.java
index 14f7fb1..66a3bfe 100644
--- a/src/tools/android/java/com/google/devtools/build/android/xml/StyleableXmlResourceValue.java
+++ b/src/tools/android/java/com/google/devtools/build/android/xml/StyleableXmlResourceValue.java
@@ -235,6 +235,11 @@
   }
 
   @Override
+  public int compareMergePriorityTo(XmlResourceValue value) {
+    return 0;
+  }
+
+  @Override
   public String asConflictStringWith(DataSource source) {
     return source.asConflictString();
   }