| // 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.aapt.Resources.Item; |
| import com.android.aapt.Resources.Primitive; |
| import com.android.aapt.Resources.Reference; |
| import com.android.aapt.Resources.StyledString; |
| import com.android.aapt.Resources.StyledString.Span; |
| import com.android.aapt.Resources.Value; |
| import com.android.resources.ResourceType; |
| import com.google.common.base.MoreObjects; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.xml.XmlEscapers; |
| import com.google.devtools.build.android.AndroidDataWritingVisitor; |
| import com.google.devtools.build.android.AndroidDataWritingVisitor.StartTag; |
| import com.google.devtools.build.android.AndroidResourceSymbolSink; |
| import com.google.devtools.build.android.DataSource; |
| import com.google.devtools.build.android.DependencyInfo; |
| 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 com.google.devtools.build.android.proto.SerializeFormat.DataValueXml; |
| import com.google.devtools.build.android.resources.Visibility; |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.util.Arrays; |
| import java.util.Locale; |
| import java.util.Objects; |
| import javax.annotation.Nullable; |
| import javax.annotation.concurrent.Immutable; |
| import javax.xml.namespace.QName; |
| |
| /** |
| * Represents a simple Android resource xml value. |
| * |
| * <p>There is a class of resources that are simple name/value pairs: string |
| * (http://developer.android.com/guide/topics/resources/string-resource.html), bool |
| * (http://developer.android.com/guide/topics/resources/more-resources.html#Bool), color |
| * (http://developer.android.com/guide/topics/resources/more-resources.html#Color), and dimen |
| * (http://developer.android.com/guide/topics/resources/more-resources.html#Dimension). These are |
| * defined in xml as <<em>resource type</em> name="<em>name</em>" value="<em>value</em>">. In |
| * the interest of keeping the parsing svelte, these are represented by a single class. |
| */ |
| @Immutable |
| public class SimpleXmlResourceValue implements XmlResourceValue { |
| |
| static final QName TAG_BOOL = QName.valueOf("bool"); |
| static final QName TAG_COLOR = QName.valueOf("color"); |
| static final QName TAG_DIMEN = QName.valueOf("dimen"); |
| static final QName TAG_DRAWABLE = QName.valueOf("drawable"); |
| static final QName TAG_FRACTION = QName.valueOf("fraction"); |
| static final QName TAG_INTEGER = QName.valueOf("integer"); |
| static final QName TAG_ITEM = QName.valueOf("item"); |
| static final QName TAG_LAYOUT = QName.valueOf("layout"); |
| static final QName TAG_MENU = QName.valueOf("menu"); |
| static final QName TAG_MIPMAP = QName.valueOf("mipmap"); |
| static final QName TAG_NAVIGATION = QName.valueOf("navigation"); |
| static final QName TAG_PUBLIC = QName.valueOf("public"); |
| static final QName TAG_RAW = QName.valueOf("raw"); |
| static final QName TAG_STRING = QName.valueOf("string"); |
| static final QName TAG_OVERLAYABLE = QName.valueOf("overlayable"); |
| |
| /** Provides an enumeration resource type and simple value validation. */ |
| public enum Type { |
| BOOL(TAG_BOOL), |
| COLOR(TAG_COLOR), |
| DIMEN(TAG_DIMEN), |
| DRAWABLE(TAG_DRAWABLE), |
| FONT(TAG_ITEM), |
| FRACTION(TAG_FRACTION), |
| INTEGER(TAG_INTEGER), |
| ITEM(TAG_ITEM), |
| LAYOUT(TAG_LAYOUT), |
| MENU(TAG_MENU), |
| MIPMAP(TAG_MIPMAP), |
| NAVIGATION(TAG_NAVIGATION), |
| PUBLIC(TAG_PUBLIC), |
| RAW(TAG_RAW), |
| STRING(TAG_STRING), |
| OVERLAYABLE(TAG_OVERLAYABLE); |
| private final QName tagName; |
| |
| Type(QName tagName) { |
| this.tagName = tagName; |
| } |
| |
| public static Type from(ResourceType resourceType) { |
| for (Type valueType : values()) { |
| if (valueType.tagName.getLocalPart().equals(resourceType.getName())) { |
| return valueType; |
| } else if (resourceType.getName().equalsIgnoreCase(valueType.name())) { |
| return valueType; |
| } |
| } |
| throw new IllegalArgumentException( |
| String.format( |
| "%s resource type not found in available types: %s", |
| resourceType, Arrays.toString(values()))); |
| } |
| } |
| |
| private final Visibility visibility; |
| private final Item item; |
| private final ImmutableMap<String, String> attributes; |
| // TODO(b/112848607): remove untyped "value" String in favor of "item" above. |
| @Nullable private final String value; |
| private final Type valueType; |
| |
| public static XmlResourceValue createWithValue(Type valueType, String value) { |
| return of(valueType, ImmutableMap.<String, String>of(), value); |
| } |
| |
| public static XmlResourceValue withAttributes( |
| Type valueType, ImmutableMap<String, String> attributes) { |
| return of(valueType, attributes, null); |
| } |
| |
| public static XmlResourceValue itemWithFormattedValue( |
| ResourceType resourceType, String format, String value) { |
| return of(Type.ITEM, ImmutableMap.of("type", resourceType.getName(), "format", format), value); |
| } |
| |
| public static XmlResourceValue itemWithValue(ResourceType resourceType, String value) { |
| return of(Type.ITEM, ImmutableMap.of("type", resourceType.getName()), value); |
| } |
| |
| public static XmlResourceValue itemPlaceHolderFor(ResourceType resourceType) { |
| return withAttributes(Type.ITEM, ImmutableMap.of("type", resourceType.getName())); |
| } |
| |
| public static XmlResourceValue of( |
| Type valueType, ImmutableMap<String, String> attributes, @Nullable String value) { |
| return new SimpleXmlResourceValue( |
| Visibility.UNKNOWN, valueType, Item.getDefaultInstance(), attributes, value); |
| } |
| |
| private SimpleXmlResourceValue( |
| Visibility visibility, |
| Type valueType, |
| Item item, |
| ImmutableMap<String, String> attributes, |
| String value) { |
| this.visibility = visibility; |
| this.valueType = valueType; |
| this.item = item; |
| this.value = value; |
| this.attributes = attributes; |
| } |
| |
| @Override |
| public void write( |
| FullyQualifiedName key, DataSource source, AndroidDataWritingVisitor mergedDataWriter) { |
| |
| StartTag startTag = |
| mergedDataWriter |
| .define(key) |
| .derivedFrom(source) |
| .startTag(valueType.tagName) |
| .named(key) |
| .addAttributesFrom(attributes.entrySet()); |
| |
| if (value != null) { |
| startTag.closeTag().addCharactersOf(value).endTag().save(); |
| } else { |
| startTag.closeUnaryTag().save(); |
| } |
| } |
| |
| @SuppressWarnings("deprecation") |
| public static XmlResourceValue from(SerializeFormat.DataValueXml proto) { |
| return of( |
| Type.valueOf(proto.getValueType()), |
| ImmutableMap.copyOf(proto.getAttributeMap()), |
| proto.hasValue() ? proto.getValue() : null); |
| } |
| |
| public static XmlResourceValue from( |
| Value proto, Visibility visibility, ResourceType resourceType) { |
| Item item = proto.getItem(); |
| String stringValue = null; |
| ImmutableMap.Builder<String, String> attributes = ImmutableMap.builder(); |
| |
| if (item.hasStr()) { |
| stringValue = XmlEscapers.xmlContentEscaper().escape(item.getStr().getValue()); |
| } else if (item.hasRef()) { |
| stringValue = "@" + item.getRef().getName(); |
| attributes.put("format", "reference"); |
| } else if (item.hasStyledStr()) { |
| StyledString styledString = item.getStyledStr(); |
| StringBuilder stringBuilder = new StringBuilder(styledString.getValue()); |
| |
| for (Span span : styledString.getSpanList()) { |
| stringBuilder.append( |
| String.format(";%s,%d,%d", span.getTag(), span.getFirstChar(), span.getLastChar())); |
| } |
| stringValue = stringBuilder.toString(); |
| } else if (item.hasPrim()) { |
| stringValue = convertPrimitiveToString(item.getPrim()); |
| } else { |
| throw new IllegalArgumentException( |
| String.format("'%s' with value %s is not a simple resource type.", resourceType, proto)); |
| } |
| |
| return new SimpleXmlResourceValue( |
| visibility, |
| Type.valueOf(resourceType.toString().toUpperCase(Locale.ENGLISH)), |
| item, |
| attributes.build(), |
| stringValue); |
| } |
| |
| static String convertPrimitiveToString(Primitive primitive) { |
| switch (primitive.getOneofValueCase()) { |
| case NULL_VALUE: |
| return "(null)"; |
| case EMPTY_VALUE: |
| return "(empty)"; |
| case FLOAT_VALUE: |
| return Float.toString(primitive.getFloatValue()); |
| case INT_DECIMAL_VALUE: |
| return Integer.toString(primitive.getIntDecimalValue()); |
| case INT_HEXADECIMAL_VALUE: |
| return String.format(Locale.ROOT, "0x%x", primitive.getIntHexadecimalValue()); |
| case BOOLEAN_VALUE: |
| return Boolean.toString(primitive.getBooleanValue()); |
| case DIMENSION_VALUE: |
| return ComplexConverter.convertComplexPrimitiveToString( |
| primitive.getDimensionValue(), /*isDimension=*/ true); |
| case FRACTION_VALUE: |
| return ComplexConverter.convertComplexPrimitiveToString( |
| primitive.getFractionValue(), /*isDimension=*/ false); |
| |
| // Rendering all colors as normalized 32-bit hex. No one cares how they were written in the |
| // original XML, and rendering these differently would lead to pointless resouce conflicts |
| // reported if e.g. one XML file uses #123 while another uses #112233. |
| case COLOR_ARGB8_VALUE: |
| return String.format(Locale.ROOT, "#%08X", primitive.getColorArgb8Value()); |
| case COLOR_RGB8_VALUE: |
| return String.format(Locale.ROOT, "#%08X", primitive.getColorRgb8Value()); |
| case COLOR_ARGB4_VALUE: |
| return String.format(Locale.ROOT, "#%08X", primitive.getColorArgb4Value()); |
| case COLOR_RGB4_VALUE: |
| return String.format(Locale.ROOT, "#%08X", primitive.getColorRgb4Value()); |
| |
| case DIMENSION_VALUE_DEPRECATED: |
| case FRACTION_VALUE_DEPRECATED: |
| // we don't expect to deserialize data from older aapt2 builds |
| case ONEOFVALUE_NOT_SET: |
| break; |
| } |
| throw new IllegalArgumentException("Invalid primitive value " + primitive); |
| } |
| |
| // See 'print_complex' defined in: |
| // https://android.googlesource.com/platform/frameworks/base/+/master/libs/androidfw/ResourceTypes.cpp |
| private static final class ComplexConverter { |
| static final String[] DIMENSION_TYPE_STRINGS = |
| new String[] {"px", "dp", "sp", "pt", "in", "mm"}; |
| static final String[] FRACTION_TYPE_STRINGS = new String[] {"%", "%p"}; |
| static final int[] RADIX_SHIFTS = new int[] {0, 7, 15, 23}; |
| |
| static String convertComplexPrimitiveToString(int rawValue, boolean isDimension) { |
| String typeString; |
| try { |
| if (isDimension) { |
| typeString = DIMENSION_TYPE_STRINGS[rawValue & 0xF]; |
| } else { |
| typeString = FRACTION_TYPE_STRINGS[rawValue & 0xF]; |
| } |
| } catch (IndexOutOfBoundsException e) { |
| typeString = " (unknown unit)"; |
| } |
| |
| int radixIdx = (rawValue >> 4) & 0x3; |
| float value = (rawValue >> 8) * (1.0f / (1 << RADIX_SHIFTS[radixIdx])); |
| |
| // Use Float.toString instead of String.format("%f") to avoid excessive trailing zeros. |
| // Strings should never be localized anyway. |
| // * https://stackoverflow.com/a/44202755 |
| // * https://issuetracker.google.com/issues/64962882 |
| return String.format(Locale.ROOT, "%s%s", Float.toString(value), typeString); |
| } |
| } |
| |
| @Override |
| public void writeResourceToClass( |
| DependencyInfo dependencyInfo, FullyQualifiedName key, AndroidResourceSymbolSink sink) { |
| sink.acceptSimpleResource(dependencyInfo, visibility, key.type(), key.name()); |
| } |
| |
| @Override |
| public int serializeTo(int sourceId, Namespaces namespaces, OutputStream output) |
| throws IOException { |
| SerializeFormat.DataValue.Builder builder = |
| XmlResourceValues.newSerializableDataValueBuilder(sourceId); |
| DataValueXml.Builder xmlValueBuilder = |
| builder |
| .getXmlValueBuilder() |
| .putAllNamespace(namespaces.asMap()) |
| .setType(SerializeFormat.DataValueXml.XmlType.SIMPLE) |
| // TODO(corysmith): Find a way to avoid writing strings to the serialized format |
| // it's inefficient use of space and costs more when deserializing. |
| .putAllAttribute(attributes); |
| if (value != null) { |
| xmlValueBuilder.setValue(value); |
| } |
| builder.setXmlValue(xmlValueBuilder.setValueType(valueType.name())); |
| return XmlResourceValues.serializeProtoDataValue(output, builder); |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hash(visibility, valueType, attributes, value); |
| } |
| |
| @Override |
| public boolean equals(Object obj) { |
| if (!(obj instanceof SimpleXmlResourceValue)) { |
| return false; |
| } |
| SimpleXmlResourceValue other = (SimpleXmlResourceValue) obj; |
| return Objects.equals(visibility, other.visibility) |
| && Objects.equals(valueType, other.valueType) |
| && Objects.equals(attributes, other.attributes) |
| // TODO(b/112848607): include the "item" proto in comparison; right now it's redundant. |
| && Objects.equals(value, other.value); |
| } |
| |
| @Override |
| public String toString() { |
| return MoreObjects.toStringHelper(getClass()) |
| .add("valueType", valueType) |
| .add("attributes", attributes) |
| .add("value", value) |
| .toString(); |
| } |
| |
| @Override |
| 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) { |
| if (value != null) { |
| return String.format(" %s (with value %s)", source.asConflictString(), value); |
| } |
| return source.asConflictString(); |
| } |
| |
| @Override |
| public Visibility getVisibility() { |
| return visibility; |
| } |
| |
| @Override |
| public ImmutableList<Reference> getReferencedResources() { |
| return item.hasRef() ? ImmutableList.of(item.getRef()) : ImmutableList.of(); |
| } |
| } |