com.google.devtools.build.android.PlaceholderIdFieldInitializerBuilder.java Source code

Java tutorial

Introduction

Here is the source code for com.google.devtools.build.android.PlaceholderIdFieldInitializerBuilder.java

Source

// 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".
        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().name());
            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));
            }
            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();
    }
}