blob: 571a59803b0079c515186e5fc5d0a943a416dc2d [file] [log] [blame]
// Copyright 2017 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;
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.Maps;
import com.google.common.collect.Ordering;
import com.google.devtools.build.android.AndroidFrameworkAttrIdProvider.AttrLookupException;
import com.google.devtools.build.android.resources.FieldInitializer;
import com.google.devtools.build.android.resources.FieldInitializers;
import com.google.devtools.build.android.resources.IntArrayFieldInitializer;
import com.google.devtools.build.android.resources.IntFieldInitializer;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
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 {@link FieldInitializer}s placeholder unique ids. The real ids will be assigned when
* building the android_binary.
*/
class PlaceholderIdFieldInitializerBuilder {
private static final ImmutableList<ResourceType> INITIAL_TYPES =
ImmutableList.of(
ResourceType.DRAWABLE,
ResourceType.MIPMAP,
ResourceType.LAYOUT,
ResourceType.ANIM,
ResourceType.ANIMATOR,
ResourceType.TRANSITION,
ResourceType.INTERPOLATOR,
ResourceType.XML,
ResourceType.RAW);
private static final ImmutableSet<ResourceType> SPECIALLY_HANDLED_TYPES =
ImmutableSet.<ResourceType>builder()
// These types should always be handled first
.addAll(INITIAL_TYPES)
// The ATTR and STYLEABLE types are handled by completely separate code and should not be
// included in the ordered list of types
.add(ResourceType.ATTR)
.add(ResourceType.STYLEABLE)
// The MENU type should always go last
.add(ResourceType.MENU)
.build();
/**
* Determine the TT portion of the resource ID (PPTTEEEE) that aapt would have assigned. This not
* at all alphabetical. It depends on the order in which the types are processed, and whether or
* not previous types are present (compact). See the code in aapt Resource.cpp:buildResources().
* There are several seemingly arbitrary and different processing orders in the function, but the
* ordering is determined specifically by the portion at: <a
* href="https://android.googlesource.com/platform/frameworks/base.git/+/marshmallow-release/tools/aapt/Resource.cpp#1254">
* Resource.cpp:buildResources() </a>
*
* <p>where it does:
*
* <pre>
* if (drawables != NULL) { ... }
* if (mipmaps != NULL) { ... }
* if (layouts != NULL) { ... }
* </pre>
*
* Numbering starts at 1 instead of 0, and ResourceType.ATTR comes before the rest.
* ResourceType.STYLEABLE doesn't actually need a resource ID, so that is skipped. We encode the
* ordering in the following list.
*/
private static final ImmutableList<ResourceType> AAPT_TYPE_ORDERING =
ImmutableList.<ResourceType>builder()
.addAll(INITIAL_TYPES)
// The VALUES portion
// Technically, aapt just assigns according to declaration order in the source value.xml
// files so it isn't really deterministic. However, the Gradle merger sorts the values.xml
// file before invoking aapt, so use the alphabetically sorted values defined in
// ResourceType here as well.
.addAll(
Arrays.stream(ResourceType.values())
.filter((x) -> !SPECIALLY_HANDLED_TYPES.contains(x))
.collect(ImmutableList.toImmutableList()))
// Technically, file-based COLOR resources come next. If we care about complete
// equivalence we should separate the file-based resources from value-based resources so
// that we can number them the same way.
.add(ResourceType.MENU)
.build();
private static final int APP_PACKAGE_MASK = 0x7f000000;
private static final int ATTR_TYPE_ID = 1;
private static final Logger logger =
Logger.getLogger(PlaceholderIdFieldInitializerBuilder.class.getName());
private static final String NORMALIZED_ANDROID_PREFIX = "android_";
/**
* Assign any public ids to the given idBuilder.
*
* @param nameToId 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(
Map<String, Integer> nameToId, 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()));
}
nameToId.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;
}
private static String normalizeAttrName(String attrName) {
// In addition to ".", attributes can have ":", e.g., for "android:textColor".
Preconditions.checkArgument(!attrName.contains("::"), "invalid name %s", attrName);
return normalizeName(attrName).replace(':', '_');
}
private static String normalizeName(String resourceName) {
return resourceName.replace('.', '_');
}
public static PlaceholderIdFieldInitializerBuilder from(
AndroidFrameworkAttrIdProvider androidIdProvider) {
return new PlaceholderIdFieldInitializerBuilder(androidIdProvider);
}
public static PlaceholderIdFieldInitializerBuilder from(Path androidJar) {
return from(new AndroidFrameworkAttrIdJar(androidJar));
}
private final AndroidFrameworkAttrIdProvider androidIdProvider;
private final Map<ResourceType, Set<String>> innerClasses = new EnumMap<>(ResourceType.class);
private final Map<ResourceType, SortedMap<String, Optional<Integer>>> publicIds =
new EnumMap<>(ResourceType.class);
private final Map<String, Map<String, Boolean>> styleableAttrs = new HashMap<>();
private PlaceholderIdFieldInitializerBuilder(AndroidFrameworkAttrIdProvider androidIdProvider) {
this.androidIdProvider = androidIdProvider;
}
public void addPublicResource(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 addSimpleResource(ResourceType type, String name) {
Set<String> fields = innerClasses.get(type);
if (fields == null) {
fields = new HashSet<>();
innerClasses.put(type, fields);
}
fields.add(normalizeName(name));
}
public void addStyleableResource(FullyQualifiedName key, Map<FullyQualifiedName, Boolean> attrs) {
ResourceType type = ResourceType.STYLEABLE;
// The configuration can play a role in sorting, but that isn't modeled yet.
String normalizedStyleableName = normalizeName(key.name());
addSimpleResource(type, normalizedStyleableName);
// We should have merged styleables, so there should only be one definition per configuration.
// However, we don't combine across configurations, so there can be a pre-existing definition.
Map<String, Boolean> normalizedAttrs = styleableAttrs.get(normalizedStyleableName);
if (normalizedAttrs == null) {
// We need to maintain the original order of the attrs.
normalizedAttrs = new LinkedHashMap<>();
styleableAttrs.put(normalizedStyleableName, normalizedAttrs);
}
for (Map.Entry<FullyQualifiedName, Boolean> attrEntry : attrs.entrySet()) {
String normalizedAttrName = normalizeAttrName(attrEntry.getKey().qualifiedName());
normalizedAttrs.put(normalizedAttrName, attrEntry.getValue());
}
}
private Map<String, Integer> assignAttrIds(int attrTypeId) {
// Attrs are special, since they can be defined within a declare-styleable. Those are sorted
// after top-level definitions.
if (!innerClasses.containsKey(ResourceType.ATTR)) {
return ImmutableMap.of();
}
Map<String, Integer> attrToId =
Maps.newHashMapWithExpectedSize(innerClasses.get(ResourceType.ATTR).size());
// 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(attrToId, publicIds.get(ResourceType.ATTR), attrTypeId);
}
Set<String> inlineAttrs = new HashSet<>();
Set<String> styleablesWithInlineAttrs = new TreeSet<>();
for (Map.Entry<String, Map<String, Boolean>> styleableAttrEntry : styleableAttrs.entrySet()) {
Map<String, Boolean> attrs = styleableAttrEntry.getValue();
for (Map.Entry<String, Boolean> attrEntry : attrs.entrySet()) {
if (attrEntry.getValue()) {
inlineAttrs.add(attrEntry.getKey());
styleablesWithInlineAttrs.add(styleableAttrEntry.getKey());
}
}
}
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) && !attrToId.containsKey(attr)) {
attrToId.put(attr, 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() && !attrToId.containsKey(attrEntry.getKey())) {
attrToId.put(attrEntry.getKey(), nextId);
nextId = nextFreeId(nextId + 1, assignedIds);
}
}
}
return attrToId;
}
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));
}
if (reservedTypeSlot == null) {
logger.warning(String.format("Invalid public resource of type %s - ignoring", currentType));
} else {
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;
}
public FieldInitializers build() throws AttrLookupException {
Map<ResourceType, Map<String, FieldInitializer>> initializers =
new EnumMap<>(ResourceType.class);
Map<ResourceType, Integer> typeIdMap = chooseTypeIds();
Map<String, Integer> attrAssignments = assignAttrIds(typeIdMap.get(ResourceType.ATTR));
for (Map.Entry<ResourceType, Set<String>> fieldEntries : innerClasses.entrySet()) {
ResourceType type = fieldEntries.getKey();
ImmutableList<String> sortedFields =
Ordering.natural().immutableSortedCopy(fieldEntries.getValue());
Map<String, FieldInitializer> fields;
if (type == ResourceType.STYLEABLE) {
fields = getStyleableInitializers(attrAssignments, sortedFields);
} else if (type == ResourceType.ATTR) {
fields = getAttrInitializers(attrAssignments, sortedFields);
} else {
int typeId = typeIdMap.get(type);
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."
Preconditions.checkArgument(fields.size() < (1 << 16));
initializers.put(type, fields);
}
return FieldInitializers.copyOf(initializers);
}
private Map<ResourceType, Integer> chooseTypeIds() {
// 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, 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) && !allocatedTypeIds.containsKey(t)) {
allocatedTypeIds.put(t, nextTypeId);
nextTypeId = nextFreeId(nextTypeId + 1, reservedTypeSlots);
}
}
// 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.checkState(
t == ResourceType.STYLEABLE || allocatedTypeIds.containsKey(t),
"Resource type %s is not allocated a type ID",
t);
}
return allocatedTypeIds;
}
private Map<String, FieldInitializer> getAttrInitializers(
Map<String, Integer> attrAssignments, Collection<String> sortedFields) {
ImmutableMap.Builder<String, FieldInitializer> initList = ImmutableMap.builder();
for (String field : sortedFields) {
int attrId = attrAssignments.get(field);
initList.put(field, IntFieldInitializer.of(attrId));
}
return initList.build();
}
private Map<String, FieldInitializer> getResourceInitializers(
ResourceType type, int typeId, Collection<String> sortedFields) {
ImmutableMap.Builder<String, FieldInitializer> initList = ImmutableMap.builder();
Map<String, Integer> publicNameToId = new HashMap<>();
Set<Integer> assignedIds = ImmutableSet.of();
if (publicIds.containsKey(type)) {
assignedIds = assignPublicIds(publicNameToId, publicIds.get(type), typeId);
}
int resourceIds = nextFreeId(getInitialIdForTypeId(typeId), assignedIds);
for (String field : sortedFields) {
Integer fieldValue = publicNameToId.get(field);
if (fieldValue == null) {
fieldValue = resourceIds;
resourceIds = nextFreeId(resourceIds + 1, assignedIds);
}
initList.put(field, IntFieldInitializer.of(fieldValue));
}
return initList.build();
}
private Map<String, FieldInitializer> getStyleableInitializers(
Map<String, Integer> attrAssignments, Collection<String> styleableFields)
throws AttrLookupException {
ImmutableMap.Builder<String, FieldInitializer> initList = ImmutableMap.builder();
for (String field : styleableFields) {
Set<String> attrs = styleableAttrs.get(field).keySet();
ImmutableMap.Builder<String, Integer> arrayInitValues = ImmutableMap.builder();
for (String attr : attrs) {
Integer attrId = attrAssignments.get(attr);
if (attrId == null) {
// It should be a framework resource, otherwise we don't know about the resource.
if (!attr.startsWith(NORMALIZED_ANDROID_PREFIX)) {
throw new AttrLookupException("App attribute not found: " + attr);
}
String attrWithoutPrefix = attr.substring(NORMALIZED_ANDROID_PREFIX.length());
attrId = androidIdProvider.getAttrId(attrWithoutPrefix);
}
arrayInitValues.put(attr, attrId);
}
// The styleable array should be sorted by ID value.
// Make sure that if we have android: framework attributes, their IDs are listed first.
ImmutableMap<String, Integer> arrayInitMap =
arrayInitValues.orderEntriesByValue(Ordering.<Integer>natural()).build();
initList.put(field, IntArrayFieldInitializer.of(arrayInitMap.values()));
int index = 0;
for (String attr : arrayInitMap.keySet()) {
initList.put(field + "_" + attr, IntFieldInitializer.of(index));
++index;
}
}
return initList.build();
}
}