/* * Copyright 2014-present Facebook, Inc. * * 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.facebook.buck.android.aapt; import com.facebook.buck.io.ProjectFilesystem; import com.google.common.base.Function; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.ComparisonChain; import com.google.common.collect.FluentIterable; import java.io.IOException; import java.nio.file.Path; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.Nullable; /** Represents a row from a symbols file generated by {@code aapt}. */ public class RDotTxtEntry implements Comparable<RDotTxtEntry> { public enum CustomDrawableType { NONE, CUSTOM, GRAYSCALE_IMAGE, } // Taken from http://developer.android.com/reference/android/R.html public enum RType { ANIM, ANIMATOR, ARRAY, ATTR, BOOL, COLOR, DIMEN, DRAWABLE, FRACTION, ID, INTEGER, INTERPOLATOR, LAYOUT, MENU, MIPMAP, PLURALS, RAW, STRING, STYLE, STYLEABLE, TRANSITION, XML; @Override public String toString() { return super.toString().toLowerCase(); } } public enum IdType { INT, INT_ARRAY; public static IdType from(String raw) { if (raw.equals("int")) { return INT; } else if (raw.equals("int[]")) { return INT_ARRAY; } throw new IllegalArgumentException(String.format("'%s' is not a valid ID type.", raw)); } @Override public String toString() { if (this.equals(INT)) { return "int"; } return "int[]"; } } public static final Function<String, RDotTxtEntry> TO_ENTRY = input -> { Optional<RDotTxtEntry> entry = parse(input); Preconditions.checkState(entry.isPresent(), "Could not parse R.txt entry: '%s'", input); return entry.get(); }; // An identifier for custom drawables. public static final String CUSTOM_DRAWABLE_IDENTIFIER = "#"; public static final String GRAYSCALE_IMAGE_IDENTIFIER = "G"; public static final String INT_ARRAY_SEPARATOR = ","; private static final Pattern INT_ARRAY_VALUES = Pattern.compile("\\s*\\{\\s*(\\S+)?\\s*\\}\\s*"); private static final Pattern TEXT_SYMBOLS_LINE = Pattern.compile( "(\\S+) (\\S+) (\\S+) ([^(" + CUSTOM_DRAWABLE_IDENTIFIER + "|" + GRAYSCALE_IMAGE_IDENTIFIER + ")]+)" + "( (" + CUSTOM_DRAWABLE_IDENTIFIER + "|" + GRAYSCALE_IMAGE_IDENTIFIER + "))?"); // A symbols file may look like: // // int id placeholder 0x7f020000 // int string debug_http_proxy_dialog_title 0x7f030004 // int string debug_http_proxy_hint 0x7f030005 // int string debug_http_proxy_summary 0x7f030003 // int string debug_http_proxy_title 0x7f030002 // int string debug_ssl_cert_check_summary 0x7f030001 // int string debug_ssl_cert_check_title 0x7f030000 // // Note that there are four columns of information: // - the type of the resource id (always seems to be int or int[], in practice) // - the type of the resource // - the name of the resource // - the value of the resource id // // Note that styleable attributes (f.i. "int styleable Anchor_Layout_android_gravity") should be // grouped together based on parent name ("Anchor_Layout" in example above). Since due to // naming flexibility, it is not possible to infer parent name from attribute name. That's why // we decided to introduce optional "parent" field which will keep information about parent, so // later we can properly sort all attributes. At the moment this field only used by IdType#INT // RType#STYLEABLE attributes. To make "compareTo" logic simpler, if parent is "null" - it will be // set to "name" value // // Custom drawables will have an additional column to denote them. // int drawable custom_drawable 0x07f01250 # public final IdType idType; public final RType type; public final String name; public final String idValue; public final String parent; public final CustomDrawableType customType; public RDotTxtEntry(IdType idType, RType type, String name, String idValue) { this(idType, type, name, idValue, CustomDrawableType.NONE); } public RDotTxtEntry( IdType idType, RType type, String name, String idValue, @Nullable String parent) { this(idType, type, name, idValue, CustomDrawableType.NONE, parent); } public RDotTxtEntry( IdType idType, RType type, String name, String idValue, CustomDrawableType customType) { this(idType, type, name, idValue, customType, name); } public RDotTxtEntry( IdType idType, RType type, String name, String idValue, CustomDrawableType customType, @Nullable String parent) { this.idType = idType; this.type = type; this.name = name; this.idValue = idValue; this.customType = customType; this.parent = parent != null ? parent : name; } public int getNumArrayValues() { Preconditions.checkState(idType == IdType.INT_ARRAY); Matcher matcher = INT_ARRAY_VALUES.matcher(idValue); if (!matcher.matches() || matcher.group(1) == null) { return 0; } return matcher.group(1).split(INT_ARRAY_SEPARATOR).length; } public RDotTxtEntry copyWithNewIdValue(String newIdValue) { return new RDotTxtEntry(idType, type, name, newIdValue, customType, parent); } public RDotTxtEntry copyWithNewParent(String parent) { return new RDotTxtEntry(idType, type, name, idValue, customType, parent); } public static Optional<RDotTxtEntry> parse(String rDotTxtLine) { Matcher matcher = TEXT_SYMBOLS_LINE.matcher(rDotTxtLine); if (!matcher.matches()) { return Optional.empty(); } CustomDrawableType customType = CustomDrawableType.NONE; IdType idType = IdType.from(matcher.group(1)); RType type = RType.valueOf(matcher.group(2).toUpperCase()); String name = matcher.group(3); String idValue = matcher.group(4); String custom = matcher.group(5); if (custom != null && custom.length() > 0) { custom = matcher.group(6); } if (CUSTOM_DRAWABLE_IDENTIFIER.equals(custom)) { customType = CustomDrawableType.CUSTOM; } else if (GRAYSCALE_IMAGE_IDENTIFIER.equals(custom)) { customType = CustomDrawableType.GRAYSCALE_IMAGE; } return Optional.of(new RDotTxtEntry(idType, type, name, idValue, customType)); } public static Iterable<RDotTxtEntry> readResources( ProjectFilesystem owningFilesystem, Path rDotTxt) throws IOException { return FluentIterable.from(owningFilesystem.readLines(rDotTxt)) .filter(input -> !Strings.isNullOrEmpty(input)) .transform(RDotTxtEntry.TO_ENTRY); } /** * A collection of Resources should be sorted such that Resources of the same type should be * grouped together, and should be alphabetized within that group. */ @Override public int compareTo(RDotTxtEntry that) { if (this == that) { return 0; } ComparisonChain comparisonChain = ComparisonChain.start() .compare(this.type, that.type) .compare(this.parent, that.parent) .compare(this.name, that.name); return comparisonChain.result(); } @Override public boolean equals(Object obj) { if (!(obj instanceof RDotTxtEntry)) { return false; } RDotTxtEntry that = (RDotTxtEntry) obj; return Objects.equal(this.type, that.type) && Objects.equal(this.name, that.name); } @Override public int hashCode() { return Objects.hashCode(type, name); } @Override public String toString() { return MoreObjects.toStringHelper(RDotTxtEntry.class) .add("idType", idType) .add("type", type) .add("name", name) .add("idValue", idValue) .add("parent", parent) .toString(); } }