aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/tools/android/java
diff options
context:
space:
mode:
Diffstat (limited to 'src/tools/android/java')
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/AndroidResourceClassWriter.java206
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/DataResourceXml.java13
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/FullyQualifiedName.java4
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/XmlResourceValues.java44
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/proto/serialize_format.proto7
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/xml/PublicXmlResourceValue.java176
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 &lt;public&gt; 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>
+ * &lt;resources&gt;
+ * &lt;string name="mypublic_string"&gt; Pub &lt;/string&gt;
+ * &lt;public name="mypublic_string" type="string" id="0x7f050004" /&gt;
+ * &lt;string name="myother_string"&gt; the others &lt;/string&gt;
+ * &lt;public name="myother_string" type="string" /&gt;
+ * &lt;/resources&gt;
+ * </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);
+ }
+}