// 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.AndroidResourceSymbolSink; import com.google.devtools.build.android.DataSource; 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.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, DataSource 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, AndroidResourceSymbolSink sink) { for (Entry<ResourceType, Optional<Integer>> entry : typeToId.entrySet()) { sink.acceptPublicResource(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(int sourceId, 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(sourceId); 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); } @Override public String asConflictStringWith(DataSource source) { return source.asConflictString(); } }