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

Java tutorial

Introduction

Here is the source code for com.google.devtools.build.android.AndroidCompiledDataDeserializer.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 static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Predicates.not;
import static java.util.stream.Collectors.toList;

import android.aapt.pb.internal.ResourcesInternal.CompiledFile;
import com.android.SdkConstants;
import com.android.aapt.ConfigurationOuterClass.Configuration;
import com.android.aapt.ConfigurationOuterClass.Configuration.KeysHidden;
import com.android.aapt.ConfigurationOuterClass.Configuration.NavHidden;
import com.android.aapt.ConfigurationOuterClass.Configuration.Orientation;
import com.android.aapt.ConfigurationOuterClass.Configuration.ScreenLayoutLong;
import com.android.aapt.ConfigurationOuterClass.Configuration.ScreenLayoutSize;
import com.android.aapt.ConfigurationOuterClass.Configuration.Touchscreen;
import com.android.aapt.ConfigurationOuterClass.Configuration.UiModeNight;
import com.android.aapt.ConfigurationOuterClass.Configuration.UiModeType;
import com.android.aapt.Resources;
import com.android.aapt.Resources.ConfigValue;
import com.android.aapt.Resources.Package;
import com.android.aapt.Resources.ResourceTable;
import com.android.aapt.Resources.Value;
import com.android.aapt.Resources.Visibility.Level;
import com.android.ide.common.resources.configuration.CountryCodeQualifier;
import com.android.ide.common.resources.configuration.DensityQualifier;
import com.android.ide.common.resources.configuration.FolderConfiguration;
import com.android.ide.common.resources.configuration.KeyboardStateQualifier;
import com.android.ide.common.resources.configuration.LayoutDirectionQualifier;
import com.android.ide.common.resources.configuration.LocaleQualifier;
import com.android.ide.common.resources.configuration.NavigationMethodQualifier;
import com.android.ide.common.resources.configuration.NavigationStateQualifier;
import com.android.ide.common.resources.configuration.NetworkCodeQualifier;
import com.android.ide.common.resources.configuration.NightModeQualifier;
import com.android.ide.common.resources.configuration.ResourceQualifier;
import com.android.ide.common.resources.configuration.ScreenDimensionQualifier;
import com.android.ide.common.resources.configuration.ScreenHeightQualifier;
import com.android.ide.common.resources.configuration.ScreenOrientationQualifier;
import com.android.ide.common.resources.configuration.ScreenRatioQualifier;
import com.android.ide.common.resources.configuration.ScreenRoundQualifier;
import com.android.ide.common.resources.configuration.ScreenSizeQualifier;
import com.android.ide.common.resources.configuration.ScreenWidthQualifier;
import com.android.ide.common.resources.configuration.SmallestScreenWidthQualifier;
import com.android.ide.common.resources.configuration.TextInputMethodQualifier;
import com.android.ide.common.resources.configuration.TouchScreenQualifier;
import com.android.ide.common.resources.configuration.UiModeQualifier;
import com.android.ide.common.resources.configuration.VersionQualifier;
import com.android.resources.Density;
import com.android.resources.Keyboard;
import com.android.resources.KeyboardState;
import com.android.resources.LayoutDirection;
import com.android.resources.Navigation;
import com.android.resources.NavigationState;
import com.android.resources.NightMode;
import com.android.resources.ResourceType;
import com.android.resources.ScreenOrientation;
import com.android.resources.ScreenRatio;
import com.android.resources.ScreenRound;
import com.android.resources.ScreenSize;
import com.android.resources.TouchScreen;
import com.android.resources.UiMode;
import com.google.common.base.Preconditions;
import com.google.common.base.Stopwatch;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.LittleEndianDataInputStream;
import com.google.devtools.build.android.FullyQualifiedName.Factory;
import com.google.devtools.build.android.proto.SerializeFormat;
import com.google.devtools.build.android.proto.SerializeFormat.Header;
import com.google.devtools.build.android.xml.ResourcesAttribute.AttributeType;
import com.google.protobuf.InvalidProtocolBufferException;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import javax.annotation.concurrent.NotThreadSafe;

/** Deserializes {@link DataKey}, {@link DataValue} entries from compiled resource files. */
public class AndroidCompiledDataDeserializer implements AndroidDataDeserializer {
    private static final Logger logger = Logger.getLogger(AndroidCompiledDataDeserializer.class.getName());

    static final ImmutableMap<Configuration.LayoutDirection, LayoutDirection> LAYOUT_DIRECTION_MAP = ImmutableMap
            .of(Configuration.LayoutDirection.LAYOUT_DIRECTION_LTR, LayoutDirection.LTR,
                    Configuration.LayoutDirection.LAYOUT_DIRECTION_RTL, LayoutDirection.RTL);

    static final ImmutableMap<Configuration.ScreenLayoutSize, ScreenSize> LAYOUT_SIZE_MAP = ImmutableMap.of(
            ScreenLayoutSize.SCREEN_LAYOUT_SIZE_SMALL, ScreenSize.SMALL, ScreenLayoutSize.SCREEN_LAYOUT_SIZE_NORMAL,
            ScreenSize.NORMAL, ScreenLayoutSize.SCREEN_LAYOUT_SIZE_LARGE, ScreenSize.LARGE,
            ScreenLayoutSize.SCREEN_LAYOUT_SIZE_XLARGE, ScreenSize.XLARGE);

    static final ImmutableMap<Configuration.ScreenLayoutLong, ScreenRatio> SCREEN_LONG_MAP = ImmutableMap.of(
            ScreenLayoutLong.SCREEN_LAYOUT_LONG_LONG, ScreenRatio.LONG, ScreenLayoutLong.SCREEN_LAYOUT_LONG_NOTLONG,
            ScreenRatio.NOTLONG);

    static final ImmutableMap<Configuration.ScreenRound, ScreenRound> SCREEN_ROUND_MAP = ImmutableMap.of(
            Configuration.ScreenRound.SCREEN_ROUND_ROUND, ScreenRound.ROUND,
            Configuration.ScreenRound.SCREEN_ROUND_NOTROUND, ScreenRound.NOTROUND);

    private static final ImmutableMap<Configuration.Orientation, ScreenOrientation> SCREEN_ORIENTATION_MAP = ImmutableMap
            .of(Orientation.ORIENTATION_LAND, ScreenOrientation.LANDSCAPE, Orientation.ORIENTATION_PORT,
                    ScreenOrientation.PORTRAIT, Orientation.ORIENTATION_SQUARE, ScreenOrientation.SQUARE);

    private static final ImmutableMap<UiModeType, UiMode> SCREEN_UI_MODE = ImmutableMap
            .<UiModeType, UiMode>builder().put(UiModeType.UI_MODE_TYPE_APPLIANCE, UiMode.APPLIANCE)
            .put(UiModeType.UI_MODE_TYPE_CAR, UiMode.CAR).put(UiModeType.UI_MODE_TYPE_DESK, UiMode.DESK)
            .put(UiModeType.UI_MODE_TYPE_NORMAL, UiMode.NORMAL)
            .put(UiModeType.UI_MODE_TYPE_TELEVISION, UiMode.TELEVISION)
            .put(UiModeType.UI_MODE_TYPE_VRHEADSET, UiMode.NORMAL).put(UiModeType.UI_MODE_TYPE_WATCH, UiMode.WATCH)
            .build();

    static final ImmutableMap<Configuration.UiModeNight, NightMode> NIGHT_MODE_MAP = ImmutableMap.of(
            UiModeNight.UI_MODE_NIGHT_NIGHT, NightMode.NIGHT, UiModeNight.UI_MODE_NIGHT_NOTNIGHT,
            NightMode.NOTNIGHT);

    static final ImmutableMap<Configuration.KeysHidden, KeyboardState> KEYBOARD_STATE_MAP = ImmutableMap.of(
            KeysHidden.KEYS_HIDDEN_KEYSEXPOSED, KeyboardState.EXPOSED, KeysHidden.KEYS_HIDDEN_KEYSSOFT,
            KeyboardState.SOFT, KeysHidden.KEYS_HIDDEN_KEYSHIDDEN, KeyboardState.HIDDEN);

    static final ImmutableMap<Configuration.Touchscreen, TouchScreen> TOUCH_TYPE_MAP = ImmutableMap.of(
            Touchscreen.TOUCHSCREEN_FINGER, TouchScreen.FINGER, Touchscreen.TOUCHSCREEN_NOTOUCH,
            TouchScreen.NOTOUCH, Touchscreen.TOUCHSCREEN_STYLUS, TouchScreen.STYLUS);

    static final ImmutableMap<Configuration.Keyboard, Keyboard> KEYBOARD_MAP = ImmutableMap.of(
            Configuration.Keyboard.KEYBOARD_NOKEYS, Keyboard.NOKEY, Configuration.Keyboard.KEYBOARD_QWERTY,
            Keyboard.QWERTY, Configuration.Keyboard.KEYBOARD_TWELVEKEY, Keyboard.TWELVEKEY);

    static final ImmutableMap<Configuration.NavHidden, NavigationState> NAV_STATE_MAP = ImmutableMap.of(
            NavHidden.NAV_HIDDEN_NAVHIDDEN, NavigationState.HIDDEN, NavHidden.NAV_HIDDEN_NAVEXPOSED,
            NavigationState.EXPOSED);

    static final ImmutableMap<Configuration.Navigation, Navigation> NAVIGATION_MAP = ImmutableMap.of(
            Configuration.Navigation.NAVIGATION_DPAD, Navigation.DPAD, Configuration.Navigation.NAVIGATION_NONAV,
            Navigation.NONAV, Configuration.Navigation.NAVIGATION_TRACKBALL, Navigation.TRACKBALL,
            Configuration.Navigation.NAVIGATION_WHEEL, Navigation.WHEEL);

    static final ImmutableMap<Integer, Density> DENSITY_MAP = ImmutableMap.<Integer, Density>builder()
            .put(0xfffe, Density.ANYDPI).put(0xffff, Density.NODPI).put(120, Density.LOW).put(160, Density.MEDIUM)
            .put(213, Density.TV).put(240, Density.HIGH).put(320, Density.XHIGH).put(480, Density.XXHIGH)
            .put(640, Density.XXXHIGH).build();

    private final ImmutableSet<String> filteredResources;

    /**
     * @param filteredResources resources that were filtered out of this target and should be ignored
     *     if they are referenced in symbols files.
     */
    public static AndroidCompiledDataDeserializer withFilteredResources(Collection<String> filteredResources) {
        return new AndroidCompiledDataDeserializer(ImmutableSet.copyOf(filteredResources));
    }

    public static AndroidCompiledDataDeserializer create() {
        return new AndroidCompiledDataDeserializer(ImmutableSet.of());
    }

    private AndroidCompiledDataDeserializer(ImmutableSet<String> filteredResources) {
        this.filteredResources = filteredResources;
    }

    private void readResourceTable(LittleEndianDataInputStream resourceTableStream, KeyValueConsumers consumers)
            throws IOException {
        long alignedSize = resourceTableStream.readLong();
        Preconditions.checkArgument(alignedSize <= Integer.MAX_VALUE);

        byte[] tableBytes = new byte[(int) alignedSize];
        resourceTableStream.read(tableBytes, 0, (int) alignedSize);
        ResourceTable resourceTable = ResourceTable.parseFrom(tableBytes);

        readPackages(consumers, resourceTable);
    }

    private void readPackages(KeyValueConsumers consumers, ResourceTable resourceTable)
            throws UnsupportedEncodingException, InvalidProtocolBufferException {
        List<String> sourcePool = decodeSourcePool(resourceTable.getSourcePool().getData().toByteArray());
        ReferenceResolver resolver = ReferenceResolver.asRoot();

        for (int i = resourceTable.getPackageCount() - 1; i >= 0; i--) {
            Package resourceTablePackage = resourceTable.getPackage(i);

            ReferenceResolver packageResolver = resolver.resolveFor(resourceTablePackage.getPackageName());
            String packageName = resourceTablePackage.getPackageName();

            for (Resources.Type resourceFormatType : resourceTablePackage.getTypeList()) {
                ResourceType resourceType = ResourceType.getEnum(resourceFormatType.getName());

                for (Resources.Entry resource : resourceFormatType.getEntryList()) {
                    if (resource.getConfigValueList().isEmpty()
                            && resource.getVisibility().getLevel() == Level.PUBLIC) {

                        // This is a public resource definition.
                        int sourceIndex = resource.getVisibility().getSource().getPathIdx();

                        String source = sourcePool.get(sourceIndex);
                        DataSource dataSource = DataSource.of(Paths.get(source));

                        DataResourceXml dataResourceXml = DataResourceXml.fromPublic(dataSource, resourceType,
                                resource.getEntryId().getId());
                        final FullyQualifiedName fqn = createAndRecordFqn(packageResolver, packageName,
                                resourceType, resource, ImmutableList.of());
                        consumers.combiningConsumer.accept(fqn, dataResourceXml);
                    } else if (!"android".equals(packageName)) {
                        // This means this resource is not in the android sdk, add it to the set.
                        for (ConfigValue configValue : resource.getConfigValueList()) {
                            FullyQualifiedName fqn = createAndRecordFqn(packageResolver, packageName, resourceType,
                                    resource, convertToQualifiers(configValue));

                            int sourceIndex = configValue.getValue().getSource().getPathIdx();

                            String source = sourcePool.get(sourceIndex);
                            DataSource dataSource = DataSource.of(Paths.get(source));

                            Value resourceValue = configValue.getValue();

                            DataResource dataResource = resourceValue.getItem().hasFile()
                                    ? DataValueFile.of(dataSource)
                                    : DataResourceXml.from(resourceValue, dataSource, resourceType,
                                            packageResolver);

                            if (!fqn.isOverwritable()) {
                                consumers.combiningConsumer.accept(fqn, dataResource);
                            } else {
                                consumers.overwritingConsumer.accept(fqn, dataResource);
                            }
                        }
                    } else {
                        // In the sdk, just add the fqn for styleables
                        createAndRecordFqn(packageResolver, packageName, resourceType, resource, ImmutableList.of())
                                .toPrettyString();
                    }
                }
            }
        }
    }

    /** Maintains state for all references in each package of a resource table. */
    @NotThreadSafe
    public static class ReferenceResolver {

        enum InlineStatus {
            INLINEABLE, INLINED,
        }

        private final Optional<String> packageName;
        private final Map<FullyQualifiedName, InlineStatus> qualifiedReferenceInlineStatus;

        private ReferenceResolver(Optional<String> packageName,
                Map<FullyQualifiedName, InlineStatus> qualifiedReferenceInlineStatus) {
            this.packageName = packageName;
            this.qualifiedReferenceInlineStatus = qualifiedReferenceInlineStatus;
        }

        static ReferenceResolver asRoot() {
            return new ReferenceResolver(Optional.empty(), new HashMap<>());
        }

        public ReferenceResolver resolveFor(String packageName) {
            return new ReferenceResolver(Optional.of(packageName).filter(not(String::isEmpty)),
                    qualifiedReferenceInlineStatus);
        }

        public FullyQualifiedName parse(String reference) {
            return FullyQualifiedName.fromReference(reference, packageName);
        }

        public FullyQualifiedName register(FullyQualifiedName fullyQualifiedName) {
            // The default is that the name can be inlined.
            qualifiedReferenceInlineStatus.put(fullyQualifiedName, InlineStatus.INLINEABLE);
            return fullyQualifiedName;
        }

        /** Indicates if a reference can be inlined in a styleable. */
        public boolean shouldInline(FullyQualifiedName reference) {
            return checkNotNull(qualifiedReferenceInlineStatus.get(reference),
                    "%s reference is unsatisfied. Available names: %s", reference,
                    qualifiedReferenceInlineStatus.keySet()).equals(InlineStatus.INLINEABLE)
                    // Only inline if it's in the current package.
                    && reference.isInPackage(packageName.orElse(FullyQualifiedName.DEFAULT_PACKAGE));
        }

        /** Update the reference's inline state. */
        public FullyQualifiedName markInlined(FullyQualifiedName reference) {
            qualifiedReferenceInlineStatus.put(reference, InlineStatus.INLINED);
            return reference;
        }
    }

    private FullyQualifiedName createAndRecordFqn(ReferenceResolver packageResolver, String packageName,
            ResourceType resourceType, Resources.Entry resource, List<String> qualifiers) {
        final FullyQualifiedName fqn = FullyQualifiedName.of(
                packageName.isEmpty() ? FullyQualifiedName.DEFAULT_PACKAGE : packageName, qualifiers, resourceType,
                resource.getName());
        packageResolver.register(fqn);
        return fqn;
    }

    private List<String> convertToQualifiers(ConfigValue configValue) {
        FolderConfiguration configuration = new FolderConfiguration();
        final Configuration protoConfig = configValue.getConfig();
        if (protoConfig.getMcc() > 0) {
            configuration.setCountryCodeQualifier(new CountryCodeQualifier(protoConfig.getMcc()));
        }
        // special code for 0, as MNC can be zero
        // https://android.googlesource.com/platform/frameworks/native/+/master/include/android/configuration.h#473
        if (protoConfig.getMnc() != 0) {
            configuration.setNetworkCodeQualifier(NetworkCodeQualifier.getQualifier(String.format(Locale.US,
                    "mnc%1$03d", protoConfig.getMnc() == 0xffff ? 0 : protoConfig.getMnc())));
        }

        if (!protoConfig.getLocale().isEmpty()) {
            // The proto stores it in a BCP-47 format, but the parser requires a b+ and all the - as +.
            // It's a nice a little impedance mismatch.
            new LocaleQualifier().checkAndSet("b+" + protoConfig.getLocale().replaceAll("-", "+"), configuration);
        }

        if (LAYOUT_DIRECTION_MAP.containsKey(protoConfig.getLayoutDirection())) {
            configuration.setLayoutDirectionQualifier(
                    new LayoutDirectionQualifier(LAYOUT_DIRECTION_MAP.get(protoConfig.getLayoutDirection())));
        }

        if (protoConfig.getSmallestScreenWidthDp() > 0) {
            configuration.setSmallestScreenWidthQualifier(
                    new SmallestScreenWidthQualifier(protoConfig.getSmallestScreenWidthDp()));
        }

        // screen dimension is defined if one number is greater than 0
        if (Math.max(protoConfig.getScreenHeight(), protoConfig.getScreenWidth()) > 0) {
            configuration.setScreenDimensionQualifier(new ScreenDimensionQualifier(
                    Math.max(protoConfig.getScreenHeight(), protoConfig.getScreenWidth()), // biggest is always first
                    Math.min(protoConfig.getScreenHeight(), protoConfig.getScreenWidth())));
        }

        if (protoConfig.getScreenWidthDp() > 0) {
            configuration.setScreenWidthQualifier(new ScreenWidthQualifier(protoConfig.getScreenWidthDp()));
        }

        if (protoConfig.getScreenHeightDp() > 0) {
            configuration.setScreenHeightQualifier(new ScreenHeightQualifier(protoConfig.getScreenHeightDp()));
        }

        if (LAYOUT_SIZE_MAP.containsKey(protoConfig.getScreenLayoutSize())) {
            configuration.setScreenSizeQualifier(
                    new ScreenSizeQualifier(LAYOUT_SIZE_MAP.get(protoConfig.getScreenLayoutSize())));
        }

        if (SCREEN_LONG_MAP.containsKey(protoConfig.getScreenLayoutLong())) {
            configuration.setScreenRatioQualifier(
                    new ScreenRatioQualifier(SCREEN_LONG_MAP.get(protoConfig.getScreenLayoutLong())));
        }

        if (SCREEN_ROUND_MAP.containsKey(protoConfig.getScreenRound())) {
            configuration.setScreenRoundQualifier(
                    new ScreenRoundQualifier(SCREEN_ROUND_MAP.get(protoConfig.getScreenRound())));
        }

        if (SCREEN_ORIENTATION_MAP.containsKey(protoConfig.getOrientation())) {
            configuration.setScreenOrientationQualifier(
                    new ScreenOrientationQualifier(SCREEN_ORIENTATION_MAP.get(protoConfig.getOrientation())));
        }

        if (SCREEN_UI_MODE.containsKey(protoConfig.getUiModeType())) {
            configuration.setUiModeQualifier(new UiModeQualifier(SCREEN_UI_MODE.get(protoConfig.getUiModeType())));
        }

        if (NIGHT_MODE_MAP.containsKey(protoConfig.getUiModeNight())) {
            configuration.setNightModeQualifier(
                    new NightModeQualifier(NIGHT_MODE_MAP.get(protoConfig.getUiModeNight())));
        }

        if (DENSITY_MAP.containsKey(protoConfig.getDensity())) {
            configuration.setDensityQualifier(new DensityQualifier(DENSITY_MAP.get(protoConfig.getDensity())));
        }

        if (TOUCH_TYPE_MAP.containsKey(protoConfig.getTouchscreen())) {
            configuration.setTouchTypeQualifier(
                    new TouchScreenQualifier(TOUCH_TYPE_MAP.get(protoConfig.getTouchscreen())));
        }

        if (KEYBOARD_STATE_MAP.containsKey(protoConfig.getKeysHidden())) {
            configuration.setKeyboardStateQualifier(
                    new KeyboardStateQualifier(KEYBOARD_STATE_MAP.get(protoConfig.getKeysHidden())));
        }

        if (KEYBOARD_MAP.containsKey(protoConfig.getKeyboard())) {
            configuration.setTextInputMethodQualifier(
                    new TextInputMethodQualifier(KEYBOARD_MAP.get(protoConfig.getKeyboard())));
        }

        if (NAV_STATE_MAP.containsKey(protoConfig.getNavHidden())) {
            configuration.setNavigationStateQualifier(
                    new NavigationStateQualifier(NAV_STATE_MAP.get(protoConfig.getNavHidden())));
        }

        if (NAVIGATION_MAP.containsKey(protoConfig.getNavigation())) {
            configuration.setNavigationMethodQualifier(
                    new NavigationMethodQualifier(NAVIGATION_MAP.get(protoConfig.getNavigation())));
        }

        if (protoConfig.getSdkVersion() > 0) {
            configuration.setVersionQualifier(new VersionQualifier(protoConfig.getSdkVersion()));
        }

        return Arrays.stream(configuration.getQualifiers()).map(ResourceQualifier::getFolderSegment)
                .collect(toList());
    }

    /**
     * Reads compiled resource data files and adds them to consumers
     *
     * @param compiledFileStream First byte is number of compiled files represented in this file. Next
     *     8 bytes is a long indicating the length of the metadata describing the compiled file. Next
     *     N bytes is the metadata describing the compiled file. The remaining bytes are the actual
     *     original file.
     * @param consumers
     * @param fqnFactory
     * @throws IOException
     */
    private void readCompiledFile(LittleEndianDataInputStream compiledFileStream, KeyValueConsumers consumers,
            Factory fqnFactory) throws IOException {
        // Skip aligned size. We don't need it here.
        Preconditions.checkArgument(compiledFileStream.skipBytes(8) == 8);

        int resFileHeaderSize = compiledFileStream.readInt();

        // Skip data payload size. We don't need it here.
        Preconditions.checkArgument(compiledFileStream.skipBytes(8) == 8);

        byte[] file = new byte[resFileHeaderSize];
        compiledFileStream.read(file, 0, resFileHeaderSize);
        CompiledFile compiledFile = CompiledFile.parseFrom(file);

        Path sourcePath = Paths.get(compiledFile.getSourcePath());
        FullyQualifiedName fqn = fqnFactory.parse(sourcePath);
        DataSource dataSource = DataSource.of(sourcePath);

        if (consumers != null) {
            consumers.overwritingConsumer.accept(fqn, DataValueFile.of(dataSource));
        }

        for (CompiledFile.Symbol exportedSymbol : compiledFile.getExportedSymbolList()) {
            if (!exportedSymbol.getResourceName().startsWith("android:")) {
                // Skip writing resource xml's for resources in the sdk
                FullyQualifiedName symbolFqn = fqnFactory.create(ResourceType.ID,
                        exportedSymbol.getResourceName().replaceFirst("id/", ""));

                DataResourceXml dataResourceXml = DataResourceXml.from(null, dataSource, ResourceType.ID, null);
                consumers.combiningConsumer.accept(symbolFqn, dataResourceXml);
            }
        }
    }

    private void readAttributesFile(InputStream resourceFileStream, FileSystem fileSystem,
            KeyValueConsumers consumers) throws IOException {

        Header header = Header.parseDelimitedFrom(resourceFileStream);
        List<DataKey> fullyQualifiedNames = new ArrayList<>();
        for (int i = 0; i < header.getEntryCount(); i++) {
            SerializeFormat.DataKey protoKey = SerializeFormat.DataKey.parseDelimitedFrom(resourceFileStream);
            fullyQualifiedNames.add(FullyQualifiedName.fromProto(protoKey));
        }

        DataSourceTable sourceTable = DataSourceTable.read(resourceFileStream, fileSystem, header);

        for (DataKey fullyQualifiedName : fullyQualifiedNames) {
            SerializeFormat.DataValue protoValue = SerializeFormat.DataValue.parseDelimitedFrom(resourceFileStream);
            DataSource source = sourceTable.sourceFromId(protoValue.getSourceId());
            DataResourceXml dataResourceXml = (DataResourceXml) DataResourceXml.from(protoValue, source);
            AttributeType attributeType = AttributeType.valueOf(protoValue.getXmlValue().getValueType());

            if (attributeType.isCombining()) {
                consumers.combiningConsumer.accept(fullyQualifiedName, dataResourceXml);
            } else {
                consumers.overwritingConsumer.accept(fullyQualifiedName, dataResourceXml);
            }
        }
    }

    public void readTable(InputStream in, KeyValueConsumers consumers) throws IOException {
        final ResourceTable resourceTable = ResourceTable.parseFrom(in);
        readPackages(consumers, resourceTable);
    }

    @Override
    public void read(Path inPath, KeyValueConsumers consumers) {
        Stopwatch timer = Stopwatch.createStarted();
        try (ZipFile zipFile = new ZipFile(inPath.toFile())) {
            Enumeration<? extends ZipEntry> resourceFiles = zipFile.entries();

            while (resourceFiles.hasMoreElements()) {
                ZipEntry resourceFile = resourceFiles.nextElement();
                String fileZipPath = resourceFile.getName();
                int resourceSubdirectoryIndex = fileZipPath.indexOf('_', fileZipPath.lastIndexOf('/'));
                Path filePath = Paths
                        .get(String.format("%s%c%s", fileZipPath.substring(0, resourceSubdirectoryIndex), '/',
                                fileZipPath.substring(resourceSubdirectoryIndex + 1)));

                String shortPath = filePath.getParent().getFileName() + "/" + filePath.getFileName();

                if (filteredResources.contains(shortPath) && !Files.exists(filePath)) {
                    // Skip files that were filtered out during analysis.
                    // TODO(asteinb): Properly filter out these files from android_library symbol files during
                    // analysis instead, and remove this list.
                    continue;
                }

                try (InputStream resourceFileStream = zipFile.getInputStream(resourceFile)) {
                    final String[] dirNameAndQualifiers = filePath.getParent().getFileName().toString()
                            .split(SdkConstants.RES_QUALIFIER_SEP);
                    Factory fqnFactory = Factory.fromDirectoryName(dirNameAndQualifiers);

                    if (fileZipPath.endsWith(".attributes")) {
                        readAttributesFile(resourceFileStream, inPath.getFileSystem(), consumers);
                    } else {
                        LittleEndianDataInputStream dataInputStream = new LittleEndianDataInputStream(
                                resourceFileStream);

                        int magicNumber = dataInputStream.readInt();
                        int formatVersion = dataInputStream.readInt();
                        int numberOfEntries = dataInputStream.readInt();
                        int resourceType = dataInputStream.readInt();

                        if (resourceType == 0) { // 0 is a resource table
                            readResourceTable(dataInputStream, consumers);
                        } else if (resourceType == 1) { // 1 is a resource file
                            readCompiledFile(dataInputStream, consumers, fqnFactory);
                        } else {
                            throw new DeserializationException("aapt2 version mismatch.",
                                    new DeserializationException(String.format(
                                            "Unexpected tag for resourceType %s expected 0 or 1 in %s."
                                                    + "\n Last known good values:"
                                                    + "\n\tmagicNumber 1414545729 (is %s)"
                                                    + "\n\tformatVersion 1 (is %s)"
                                                    + "\n\tnumberOfEntries 1 (is %s)",
                                            resourceType, fileZipPath, magicNumber, formatVersion,
                                            numberOfEntries)));
                        }
                    }
                }
            }
        } catch (IOException e) {
            throw new DeserializationException("Error deserializing " + inPath, e);
        } finally {
            logger.fine(
                    String.format("Deserialized in compiled merged in %sms", timer.elapsed(TimeUnit.MILLISECONDS)));
        }
    }

    private static List<String> decodeSourcePool(byte[] bytes) throws UnsupportedEncodingException {
        ByteBuffer byteBuffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);

        int stringCount = byteBuffer.getInt(8);
        boolean isUtf8 = (byteBuffer.getInt(16) & (1 << 8)) != 0;
        int stringsStart = byteBuffer.getInt(20);
        // Position the ByteBuffer after the metadata
        byteBuffer.position(28);

        List<String> strings = new ArrayList<>();

        for (int i = 0; i < stringCount; i++) {
            int stringOffset = stringsStart + byteBuffer.getInt();

            if (isUtf8) {
                int characterCount = byteBuffer.get(stringOffset) & 0xFF;
                if ((characterCount & 0x80) != 0) {
                    characterCount = ((characterCount & 0x7F) << 8) | (byteBuffer.get(stringOffset + 1) & 0xFF);
                }

                stringOffset += (characterCount >= (0x80) ? 2 : 1);

                int length = byteBuffer.get(stringOffset) & 0xFF;
                if ((length & 0x80) != 0) {
                    length = ((length & 0x7F) << 8) | (byteBuffer.get(stringOffset + 1) & 0xFF);
                }

                stringOffset += (length >= (0x80) ? 2 : 1);

                strings.add(new String(bytes, stringOffset, length, "UTF8"));
            } else {
                int characterCount = byteBuffer.get(stringOffset) & 0xFFFF;
                if ((characterCount & 0x8000) != 0) {
                    characterCount = ((characterCount & 0x7FFF) << 16)
                            | (byteBuffer.get(stringOffset + 2) & 0xFFFF);
                }

                stringOffset += 2 * (characterCount >= (0x8000) ? 2 : 1);

                int length = byteBuffer.get(stringOffset) & 0xFFFF;
                if ((length & 0x8000) != 0) {
                    length = ((length & 0x7FFF) << 16) | (byteBuffer.get(stringOffset + 2) & 0xFFFF);
                }

                stringOffset += 2 * (length >= (0x8000) ? 2 : 1);

                strings.add(new String(bytes, stringOffset, length, "UTF16"));
            }
        }

        return strings;
    }
}