diff options
Diffstat (limited to 'src/tools/android/java')
6 files changed, 420 insertions, 30 deletions
diff --git a/src/tools/android/java/com/google/devtools/build/android/AndroidResourceClassWriter.java b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceClassWriter.java index 0d51f50d2e..4c3118e61b 100644 --- a/src/tools/android/java/com/google/devtools/build/android/AndroidResourceClassWriter.java +++ b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceClassWriter.java @@ -17,9 +17,11 @@ import static java.nio.charset.StandardCharsets.UTF_8; import com.android.SdkConstants; import com.android.resources.ResourceType; +import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Ordering; import com.google.devtools.build.android.AndroidFrameworkAttrIdProvider.AttrLookupException; import com.google.devtools.build.android.resources.FieldInitializer; @@ -39,7 +41,10 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; import java.util.TreeSet; +import java.util.logging.Logger; /** * Generates the R class for an android_library with made up field initializers for the ids. The @@ -50,12 +55,18 @@ import java.util.TreeSet; */ public class AndroidResourceClassWriter implements Flushable { + private static final Logger logger = + Logger.getLogger(AndroidResourceClassWriter.class.getName()); + private static final int APP_PACKAGE_MASK = 0x7f000000; + private static final int ATTR_TYPE_ID = 1; private final AndroidFrameworkAttrIdProvider androidIdProvider; private final Path outputBasePath; private final String packageName; private final Map<ResourceType, Set<String>> innerClasses = new EnumMap<>(ResourceType.class); private final Map<String, Map<String, Boolean>> styleableAttrs = new HashMap<>(); + private final Map<ResourceType, SortedMap<String, Optional<Integer>>> publicIds = + new EnumMap<>(ResourceType.class); private static final String NORMALIZED_ANDROID_PREFIX = "android_"; @@ -77,6 +88,29 @@ public class AndroidResourceClassWriter implements Flushable { fields.add(normalizeName(name)); } + public void writePublicValue(ResourceType type, String name, Optional<Integer> value) { + SortedMap<String, Optional<Integer>> publicMappings = publicIds.get(type); + if (publicMappings == null) { + publicMappings = new TreeMap<>(); + publicIds.put(type, publicMappings); + } + Optional<Integer> oldValue = publicMappings.put(name, value); + // AAPT should issue an error, but do a bit of sanity checking here just in case. + if (oldValue != null && !oldValue.equals(value)) { + // Enforce a consistent ordering on the warning message. + Integer lower = oldValue.orNull(); + Integer higher = value.orNull(); + if (Ordering.natural().compare(oldValue.orNull(), value.orNull()) > 0) { + lower = higher; + higher = oldValue.orNull(); + } + logger.warning( + String.format( + "resource %s/%s has conflicting public identifiers (0x%x vs 0x%x)", + type, name, lower, higher)); + } + } + public void writeStyleableResource(FullyQualifiedName key, Map<FullyQualifiedName, Boolean> attrs) { ResourceType type = ResourceType.STYLEABLE; @@ -161,21 +195,83 @@ public class AndroidResourceClassWriter implements Flushable { ); private Map<ResourceType, Integer> chooseTypeIds() { - Map<ResourceType, Integer> allocatedTypeIds = new EnumMap<>(ResourceType.class); + // Go through public entries. Those may have forced certain type assignments, so take those + // into account first. + Map<ResourceType, Integer> allocatedTypeIds = assignTypeIdsForPublic(); + Set<Integer> reservedTypeSlots = ImmutableSet.copyOf(allocatedTypeIds.values()); // ATTR always takes up slot #1, even if it isn't present. - allocatedTypeIds.put(ResourceType.ATTR, 1); - // The rest are packed starting at #2. - int nextTypeId = 2; + allocatedTypeIds.put(ResourceType.ATTR, ATTR_TYPE_ID); + // The rest are packed after that. + int nextTypeId = nextFreeId(ATTR_TYPE_ID + 1, reservedTypeSlots); for (ResourceType t : AAPT_TYPE_ORDERING) { - if (innerClasses.containsKey(t)) { + if (innerClasses.containsKey(t) && !allocatedTypeIds.containsKey(t)) { allocatedTypeIds.put(t, nextTypeId); - ++nextTypeId; + nextTypeId = nextFreeId(nextTypeId + 1, reservedTypeSlots); } } - // Sanity check that everything has been assigned, except STYLEABLE. + // Sanity check that everything has been assigned, except STYLEABLE. There shouldn't be + // anything of type PUBLIC either (since that isn't a real resource). // We will need to update the list if there is a new resource type. for (ResourceType t : innerClasses.keySet()) { - Preconditions.checkArgument(t == ResourceType.STYLEABLE || allocatedTypeIds.containsKey(t)); + Preconditions.checkState( + t == ResourceType.STYLEABLE || allocatedTypeIds.containsKey(t), + "Resource type %s is not allocated a type ID", + t); + } + return allocatedTypeIds; + } + + private Map<ResourceType, Integer> assignTypeIdsForPublic() { + Map<ResourceType, Integer> allocatedTypeIds = new EnumMap<>(ResourceType.class); + if (publicIds.isEmpty()) { + return allocatedTypeIds; + } + // Keep track of the reverse mapping from Int -> Type for validation. + Map<Integer, ResourceType> assignedIds = new HashMap<>(); + for (Map.Entry<ResourceType, SortedMap<String, Optional<Integer>>> publicTypeEntry : + publicIds.entrySet()) { + ResourceType currentType = publicTypeEntry.getKey(); + Integer reservedTypeSlot = null; + String previousResource = null; + for (Map.Entry<String, Optional<Integer>> publicEntry : + publicTypeEntry.getValue().entrySet()) { + Optional<Integer> reservedId = publicEntry.getValue(); + if (!reservedId.isPresent()) { + continue; + } + Integer typePortion = extractTypeId(reservedId.get()); + if (reservedTypeSlot == null) { + reservedTypeSlot = typePortion; + previousResource = publicEntry.getKey(); + } else { + if (!reservedTypeSlot.equals(typePortion)) { + logger.warning( + String.format( + "%s has conflicting type codes for its public identifiers (%s=%s vs %s=%s)", + currentType.getName(), + previousResource, + reservedTypeSlot, + publicEntry.getKey(), + typePortion)); + } + } + } + if (currentType == ResourceType.ATTR + && reservedTypeSlot != null + && !reservedTypeSlot.equals(ATTR_TYPE_ID)) { + logger.warning( + String.format( + "Cannot force ATTR to have type code other than 0x%02x (got 0x%02x from %s)", + ATTR_TYPE_ID, reservedTypeSlot, previousResource)); + } + allocatedTypeIds.put(currentType, reservedTypeSlot); + ResourceType alreadyAssigned = assignedIds.put(reservedTypeSlot, currentType); + if (alreadyAssigned != null) { + logger.warning( + String.format( + "Multiple type names declared for public type identifier 0x%x (%s vs %s)", + reservedTypeSlot, alreadyAssigned, currentType)); + } } return allocatedTypeIds; } @@ -187,6 +283,13 @@ public class AndroidResourceClassWriter implements Flushable { if (!innerClasses.containsKey(ResourceType.ATTR)) { return attrToIdBuilder.build(); } + // After assigning public IDs, we count up monotonically, so we don't need to track additional + // assignedIds to avoid collisions (use an ImmutableSet to ensure we don't add more). + Set<Integer> assignedIds = ImmutableSet.of(); + if (publicIds.containsKey(ResourceType.ATTR)) { + assignedIds = assignPublicIds(attrToIdBuilder, publicIds.get(ResourceType.ATTR), attrTypeId); + } + ImmutableMap<String, Integer> publicAttrs = attrToIdBuilder.build(); Set<String> inlineAttrs = new HashSet<>(); Set<String> styleablesWithInlineAttrs = new TreeSet<>(); for (Map.Entry<String, Map<String, Boolean>> styleableAttrEntry @@ -199,23 +302,23 @@ public class AndroidResourceClassWriter implements Flushable { } } } - int nextId = 0x7f000000 | (attrTypeId << 16); + int nextId = nextFreeId(getInitialIdForTypeId(attrTypeId), assignedIds); // Technically, aapt assigns based on declaration order, but the merge should have sorted // the non-inline attributes, so assigning by sorted order is the same. ImmutableList<String> sortedAttrs = Ordering.natural() .immutableSortedCopy(innerClasses.get(ResourceType.ATTR)); for (String attr : sortedAttrs) { - if (!inlineAttrs.contains(attr)) { + if (!inlineAttrs.contains(attr) && !publicAttrs.containsKey(attr)) { attrToIdBuilder.put(attr, nextId); - ++nextId; + nextId = nextFreeId(nextId + 1, assignedIds); } } for (String styleable : styleablesWithInlineAttrs) { Map<String, Boolean> attrs = styleableAttrs.get(styleable); for (Map.Entry<String, Boolean> attrEntry : attrs.entrySet()) { - if (attrEntry.getValue()) { + if (attrEntry.getValue() && !publicAttrs.containsKey(attrEntry.getKey())) { attrToIdBuilder.put(attrEntry.getKey(), nextId); - ++nextId; + nextId = nextFreeId(nextId + 1, assignedIds); } } } @@ -238,7 +341,7 @@ public class AndroidResourceClassWriter implements Flushable { fields = getAttrInitializers(attrAssignments, sortedFields); } else { int typeId = typeIdMap.get(type); - fields = getResourceInitializers(typeId, sortedFields); + fields = getResourceInitializers(type, typeId, sortedFields); } // The maximum number of Java fields is 2^16. // See the JVM reference "4.11. Limitations of the Java Virtual Machine." @@ -283,10 +386,9 @@ public class AndroidResourceClassWriter implements Flushable { } private List<FieldInitializer> getAttrInitializers( - Map<String, Integer> attrAssignments, - Collection<String> fields) { + Map<String, Integer> attrAssignments, Collection<String> sortedFields) { ImmutableList.Builder<FieldInitializer> initList = ImmutableList.builder(); - for (String field : fields) { + for (String field : sortedFields) { int attrId = attrAssignments.get(field); initList.add(new IntFieldInitializer(field, attrId)); } @@ -294,13 +396,22 @@ public class AndroidResourceClassWriter implements Flushable { } private List<FieldInitializer> getResourceInitializers( - int typeId, - Collection<String> fields) { + ResourceType type, int typeId, Collection<String> sortedFields) { ImmutableList.Builder<FieldInitializer> initList = ImmutableList.builder(); - int resourceIds = 0x7f000000 | typeId << 16; - for (String field : fields) { - initList.add(new IntFieldInitializer(field, resourceIds)); - ++resourceIds; + ImmutableMap.Builder<String, Integer> publicBuilder = ImmutableMap.builder(); + Set<Integer> assignedIds = ImmutableSet.of(); + if (publicIds.containsKey(type)) { + assignedIds = assignPublicIds(publicBuilder, publicIds.get(type), typeId); + } + Map<String, Integer> publicAssignments = publicBuilder.build(); + int resourceIds = nextFreeId(getInitialIdForTypeId(typeId), assignedIds); + for (String field : sortedFields) { + Integer fieldValue = publicAssignments.get(field); + if (fieldValue == null) { + fieldValue = resourceIds; + resourceIds = nextFreeId(resourceIds + 1, assignedIds); + } + initList.add(new IntFieldInitializer(field, fieldValue)); } return initList.build(); } @@ -348,4 +459,53 @@ public class AndroidResourceClassWriter implements Flushable { return normalizeName(attrName).replace(':', '_'); } + /** + * Assign any public ids to the given idBuilder. + * + * @param idBuilder where to store the final name -> id mappings + * @param publicIds known public resources (can contain null values, if ID isn't reserved) + * @param typeId the type slot for the current resource type. + * @return the final set of assigned resource ids (includes those without apriori assignments). + */ + private static Set<Integer> assignPublicIds( + ImmutableMap.Builder<String, Integer> idBuilder, + SortedMap<String, Optional<Integer>> publicIds, + int typeId) { + HashMap<Integer, String> assignedIds = new HashMap<>(); + int prevId = getInitialIdForTypeId(typeId); + for (Map.Entry<String, Optional<Integer>> entry : publicIds.entrySet()) { + Optional<Integer> id = entry.getValue(); + if (id.isPresent()) { + prevId = id.get(); + } else { + prevId = nextFreeId(prevId + 1, assignedIds.keySet()); + } + String previousMapping = assignedIds.put(prevId, entry.getKey()); + if (previousMapping != null) { + logger.warning( + String.format( + "Multiple entry names declared for public entry identifier 0x%x (%s and %s)", + prevId, previousMapping, entry.getKey())); + } + idBuilder.put(entry.getKey(), prevId); + } + return assignedIds.keySet(); + } + + private static int extractTypeId(int fullID) { + return (fullID & 0x00FF0000) >> 16; + } + + private static int getInitialIdForTypeId(int typeId) { + return APP_PACKAGE_MASK | (typeId << 16); + } + + private static int nextFreeId(int nextSlot, Set<Integer> reservedSlots) { + // Linear search for the next free slot. This assumes that reserved <public> ids are rare. + // Otherwise we should use a NavigableSet or some other smarter data-structure. + while (reservedSlots.contains(nextSlot)) { + ++nextSlot; + } + return nextSlot; + } } 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 f5689bae33..bb874e2c6d 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 @@ -15,6 +15,7 @@ package com.google.devtools.build.android; import static com.android.resources.ResourceType.DECLARE_STYLEABLE; import static com.android.resources.ResourceType.ID; +import static com.android.resources.ResourceType.PUBLIC; import com.android.resources.ResourceType; import com.google.common.base.MoreObjects; @@ -29,6 +30,7 @@ import com.google.devtools.build.android.xml.AttrXmlResourceValue; import com.google.devtools.build.android.xml.IdXmlResourceValue; import com.google.devtools.build.android.xml.Namespaces; import com.google.devtools.build.android.xml.PluralXmlResourceValue; +import com.google.devtools.build.android.xml.PublicXmlResourceValue; import com.google.devtools.build.android.xml.SimpleXmlResourceValue; import com.google.devtools.build.android.xml.StyleXmlResourceValue; import com.google.devtools.build.android.xml.StyleableXmlResourceValue; @@ -103,9 +105,11 @@ public class DataResourceXml implements DataResource { XmlResourceValues.parseDeclareStyleable( fqnFactory, path, overwritingConsumer, combiningConsumer, eventReader, start); } else { - // Of simple resources, only IDs are combining. + // Of simple resources, only IDs and Public are combining. KeyValueConsumer<DataKey, DataResource> consumer = - resourceType == ID ? combiningConsumer : overwritingConsumer; + (resourceType == ID || resourceType == PUBLIC) + ? combiningConsumer + : overwritingConsumer; String elementName = XmlResourceValues.getElementName(start); if (elementName == null) { throw new XMLStreamException( @@ -153,6 +157,8 @@ public class DataResourceXml implements DataResource { return IdXmlResourceValue.of(); case PLURAL: return PluralXmlResourceValue.from(proto); + case PUBLIC: + return PublicXmlResourceValue.from(proto); case STYLE: return StyleXmlResourceValue.from(proto); case STYLEABLE: @@ -185,6 +191,8 @@ public class DataResourceXml implements DataResource { return XmlResourceValues.parsePlurals(eventReader, start, namespacesCollector); case ATTR: return XmlResourceValues.parseAttr(eventReader, start); + case PUBLIC: + return XmlResourceValues.parsePublic(eventReader, start, namespacesCollector); case LAYOUT: case DIMEN: case STRING: @@ -199,7 +207,6 @@ public class DataResourceXml implements DataResource { case INTERPOLATOR: case MENU: case MIPMAP: - case PUBLIC: case RAW: case STYLEABLE: case TRANSITION: diff --git a/src/tools/android/java/com/google/devtools/build/android/FullyQualifiedName.java b/src/tools/android/java/com/google/devtools/build/android/FullyQualifiedName.java index ad3fe2427e..6add521689 100644 --- a/src/tools/android/java/com/google/devtools/build/android/FullyQualifiedName.java +++ b/src/tools/android/java/com/google/devtools/build/android/FullyQualifiedName.java @@ -282,7 +282,9 @@ public class FullyQualifiedName implements DataKey { } public static boolean isOverwritable(FullyQualifiedName name) { - return !(name.resourceType == ResourceType.ID || name.resourceType == ResourceType.STYLEABLE); + return !(name.resourceType == ResourceType.ID + || name.resourceType == ResourceType.PUBLIC + || name.resourceType == ResourceType.STYLEABLE); } /** diff --git a/src/tools/android/java/com/google/devtools/build/android/XmlResourceValues.java b/src/tools/android/java/com/google/devtools/build/android/XmlResourceValues.java index 1caaff264f..7bda269b58 100644 --- a/src/tools/android/java/com/google/devtools/build/android/XmlResourceValues.java +++ b/src/tools/android/java/com/google/devtools/build/android/XmlResourceValues.java @@ -13,7 +13,9 @@ // limitations under the License. package com.google.devtools.build.android; +import com.android.SdkConstants; import com.android.resources.ResourceType; +import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; import com.google.devtools.build.android.ParsedAndroidData.KeyValueConsumer; @@ -22,6 +24,7 @@ import com.google.devtools.build.android.xml.AttrXmlResourceValue; import com.google.devtools.build.android.xml.IdXmlResourceValue; import com.google.devtools.build.android.xml.Namespaces; import com.google.devtools.build.android.xml.PluralXmlResourceValue; +import com.google.devtools.build.android.xml.PublicXmlResourceValue; import com.google.devtools.build.android.xml.SimpleXmlResourceValue; import com.google.devtools.build.android.xml.StyleXmlResourceValue; import com.google.devtools.build.android.xml.StyleableXmlResourceValue; @@ -206,6 +209,47 @@ public class XmlResourceValues { contents); } + static XmlResourceValue parsePublic( + XMLEventReader eventReader, StartElement start, Namespaces.Collector namespacesCollector) + throws XMLStreamException { + namespacesCollector.collectFrom(start); + // The tag should be unary. + if (!isEndTag(eventReader.peek(), start.getName())) { + throw new XMLStreamException( + String.format("<public> tag should be unary %s", start), start.getLocation()); + } + // The tag should have a valid type attribute, and optionally an id attribute. + ImmutableMap<String, String> attributes = ImmutableMap.copyOf(parseTagAttributes(start)); + String typeAttr = attributes.get(SdkConstants.ATTR_TYPE); + ResourceType type; + if (typeAttr != null) { + type = ResourceType.getEnum(typeAttr); + if (type == null || type == ResourceType.PUBLIC) { + throw new XMLStreamException( + String.format("<public> tag has invalid type attribute %s", start), + start.getLocation()); + } + } else { + throw new XMLStreamException( + String.format("<public> tag missing type attribute %s", start), start.getLocation()); + } + String idValueAttr = attributes.get(SdkConstants.ATTR_ID); + Optional<Integer> id = Optional.absent(); + if (idValueAttr != null) { + try { + id = Optional.of(Integer.decode(idValueAttr)); + } catch (NumberFormatException e) { + throw new XMLStreamException( + String.format("<public> has invalid id number %s", start), start.getLocation()); + } + } + if (attributes.size() > 2) { + throw new XMLStreamException( + String.format("<public> has unexpected attributes %s", start), start.getLocation()); + } + return PublicXmlResourceValue.create(type, id); + } + public static Map<String, String> parseTagAttributes(StartElement start) { // Using a map to deduplicate xmlns declarations on the attributes. Map<String, String> attributeMap = new LinkedHashMap<>(); diff --git a/src/tools/android/java/com/google/devtools/build/android/proto/serialize_format.proto b/src/tools/android/java/com/google/devtools/build/android/proto/serialize_format.proto index b248555109..0090aefe36 100644 --- a/src/tools/android/java/com/google/devtools/build/android/proto/serialize_format.proto +++ b/src/tools/android/java/com/google/devtools/build/android/proto/serialize_format.proto @@ -63,9 +63,10 @@ message DataValueXml { ATTR = 1; ID = 2; PLURAL = 3; - SIMPLE = 4; - STYLEABLE = 5; - STYLE = 6; + PUBLIC = 4; + SIMPLE = 5; + STYLEABLE = 6; + STYLE = 7; } optional XmlType type = 1; 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 new file mode 100644 index 0000000000..ec6aa91c41 --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/xml/PublicXmlResourceValue.java @@ -0,0 +1,176 @@ +// Copyright 2016 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.xml; + +import com.android.SdkConstants; +import com.android.resources.ResourceType; +import com.google.common.base.MoreObjects; +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import com.google.devtools.build.android.AndroidDataWritingVisitor; +import com.google.devtools.build.android.AndroidResourceClassWriter; +import com.google.devtools.build.android.FullyQualifiedName; +import com.google.devtools.build.android.XmlResourceValue; +import com.google.devtools.build.android.XmlResourceValues; +import com.google.devtools.build.android.proto.SerializeFormat; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Path; +import java.util.EnumMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; + +/** + * Represents an Android resource <public> xml tag. + * + * <p>This is used to declare a resource public and reserve a fixed ID for a resource. It is + * generally undocumented (update this if we ever get a doc), but used heavily by the android + * framework resources. One description of it is at the android <a + * href="http://tools.android.com/tech-docs/private-resources">tools site</a>. Public tags can be + * defined in any xml file in the values folder. <code> + * <resources> + * <string name="mypublic_string"> Pub </string> + * <public name="mypublic_string" type="string" id="0x7f050004" /> + * <string name="myother_string"> the others </string> + * <public name="myother_string" type="string" /> + * </resources> + * </code> The "id" attribute is optional if an earlier public tag has already specified an "id" + * attribute. In such cases, ID assignment will continue from the previous reserved ID. + */ +public class PublicXmlResourceValue implements XmlResourceValue { + + private final Map<ResourceType, Optional<Integer>> typeToId; + private static final String MISSING_ID_VALUE = ""; + + private PublicXmlResourceValue(Map<ResourceType, Optional<Integer>> typeToId) { + this.typeToId = typeToId; + } + + public static PublicXmlResourceValue of(Map<ResourceType, Optional<Integer>> typeToId) { + return new PublicXmlResourceValue(typeToId); + } + + public static XmlResourceValue create(ResourceType type, Optional<Integer> id) { + Map<ResourceType, Optional<Integer>> map = new EnumMap<>(ResourceType.class); + map.put(type, id); + return new PublicXmlResourceValue(map); + } + + @Override + public void write( + FullyQualifiedName key, Path source, AndroidDataWritingVisitor mergedDataWriter) { + for (Entry<ResourceType, Optional<Integer>> entry : typeToId.entrySet()) { + Integer value = entry.getValue().orNull(); + mergedDataWriter + .define(key) + .derivedFrom(source) + .startTag(ResourceType.PUBLIC.getName()) + .named(key) + .attribute(SdkConstants.ATTR_TYPE) + .setTo(entry.getKey().toString()) + .optional() + .attribute(SdkConstants.ATTR_ID) + .setTo(value == null ? null : "0x" + Integer.toHexString(value)) + .closeUnaryTag() + .save(); + } + } + + @Override + public void writeResourceToClass( + FullyQualifiedName key, AndroidResourceClassWriter resourceClassWriter) { + for (Entry<ResourceType, Optional<Integer>> entry : typeToId.entrySet()) { + resourceClassWriter.writePublicValue(entry.getKey(), key.name(), entry.getValue()); + } + } + + @Override + public int hashCode() { + return Objects.hash(typeToId); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof PublicXmlResourceValue)) { + return false; + } + PublicXmlResourceValue other = (PublicXmlResourceValue) obj; + return Objects.equals(typeToId, other.typeToId); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(getClass()).add("typeToId: ", typeToId).toString(); + } + + @SuppressWarnings("deprecation") + public static XmlResourceValue from(SerializeFormat.DataValueXml proto) { + Map<String, String> protoValues = proto.getMappedStringValue(); + ImmutableMap.Builder<ResourceType, Optional<Integer>> typeToId = ImmutableMap.builder(); + for (Entry<String, String> entry : protoValues.entrySet()) { + ResourceType type = ResourceType.getEnum(entry.getKey()); + Preconditions.checkNotNull(type); + Optional<Integer> id = + MISSING_ID_VALUE.equals(entry.getValue()) + ? Optional.<Integer>absent() + : Optional.of(Integer.decode(entry.getValue())); + typeToId.put(type, id); + } + return of(typeToId.build()); + } + + @Override + public int serializeTo(Path source, Namespaces namespaces, OutputStream output) + throws IOException { + Map<String, String> assignments = Maps.newLinkedHashMapWithExpectedSize(typeToId.size()); + for (Entry<ResourceType, Optional<Integer>> entry : typeToId.entrySet()) { + Optional<Integer> value = entry.getValue(); + String stringValue = value.isPresent() ? value.get().toString() : MISSING_ID_VALUE; + assignments.put(entry.getKey().toString(), stringValue); + } + SerializeFormat.DataValue.Builder builder = + XmlResourceValues.newSerializableDataValueBuilder(source); + builder.setXmlValue( + builder + .getXmlValueBuilder() + .setType(SerializeFormat.DataValueXml.XmlType.PUBLIC) + .putAllNamespace(namespaces.asMap()) + .putAllMappedStringValue(assignments)); + return XmlResourceValues.serializeProtoDataValue(output, builder); + } + + @Override + public XmlResourceValue combineWith(XmlResourceValue value) { + if (!(value instanceof PublicXmlResourceValue)) { + throw new IllegalArgumentException(value + "is not combinable with " + this); + } + PublicXmlResourceValue other = (PublicXmlResourceValue) value; + Map<ResourceType, Optional<Integer>> combined = new EnumMap<>(ResourceType.class); + combined.putAll(typeToId); + for (Entry<ResourceType, Optional<Integer>> entry : other.typeToId.entrySet()) { + Optional<Integer> existing = combined.get(entry.getKey()); + if (existing != null && !existing.equals(entry.getValue())) { + throw new IllegalArgumentException( + String.format( + "Public resource of type %s assigned two different id values 0x%x and 0x%x", + entry.getKey(), existing.orNull(), entry.getValue().orNull())); + } + combined.put(entry.getKey(), entry.getValue()); + } + return of(combined); + } +} |