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

Java tutorial

Introduction

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

Source

// Copyright 2015 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 java.nio.charset.StandardCharsets.UTF_8;

import android.databinding.AndroidDataBinding;
import android.databinding.cli.ProcessXmlOptions;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.builder.core.VariantConfiguration;
import com.android.builder.core.VariantType;
import com.android.builder.dependency.SymbolFileProvider;
import com.android.builder.internal.SymbolLoader;
import com.android.builder.internal.SymbolWriter;
import com.android.builder.model.AaptOptions;
import com.android.ide.common.internal.CommandLineRunner;
import com.android.ide.common.internal.ExecutorSingleton;
import com.android.ide.common.internal.LoggedErrorException;
import com.android.ide.common.internal.PngCruncher;
import com.android.ide.common.res2.MergingException;
import com.android.io.FileWrapper;
import com.android.io.StreamException;
import com.android.manifmerger.ManifestMerger2;
import com.android.manifmerger.ManifestMerger2.Invoker;
import com.android.manifmerger.ManifestMerger2.Invoker.Feature;
import com.android.manifmerger.ManifestMerger2.MergeFailureException;
import com.android.manifmerger.ManifestMerger2.MergeType;
import com.android.manifmerger.ManifestMerger2.SystemProperty;
import com.android.manifmerger.MergingReport;
import com.android.manifmerger.MergingReport.MergedManifestKind;
import com.android.manifmerger.PlaceholderHandler;
import com.android.repository.Revision;
import com.android.utils.ILogger;
import com.android.utils.Pair;
import com.android.utils.StdLogger;
import com.android.xml.AndroidManifest;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Stopwatch;
import com.google.common.base.Strings;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.devtools.build.android.Converters.ExistingPathConverter;
import com.google.devtools.build.android.Converters.RevisionConverter;
import com.google.devtools.build.android.ParsedAndroidData.Builder;
import com.google.devtools.build.android.SplitConfigurationFilter.UnrecognizedSplitsException;
import com.google.devtools.build.android.resources.RClassGenerator;
import com.google.devtools.common.options.Converters.CommaSeparatedOptionListConverter;
import com.google.devtools.common.options.Option;
import com.google.devtools.common.options.OptionsBase;
import com.google.devtools.common.options.TriState;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import javax.xml.stream.FactoryConfigurationError;
import javax.xml.stream.XMLEventFactory;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLEventWriter;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.Attribute;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;
import javax.xml.xpath.XPathExpressionException;

/**
 * Provides a wrapper around the AOSP build tools for resource processing.
 */
public class AndroidResourceProcessor {
    private static final Logger logger = Logger.getLogger(AndroidResourceProcessor.class.getName());

    /**
     * Options class containing flags for Aapt setup.
     */
    public static final class AaptConfigOptions extends OptionsBase {
        @Option(name = "buildToolsVersion", defaultValue = "null", converter = RevisionConverter.class, category = "config", help = "Version of the build tools (e.g. aapt) being used, e.g. 23.0.2")
        public Revision buildToolsVersion;

        @Option(name = "aapt", defaultValue = "null", converter = ExistingPathConverter.class, category = "tool", help = "Aapt tool location for resource packaging.")
        public Path aapt;

        @Option(name = "annotationJar", defaultValue = "null", converter = ExistingPathConverter.class, category = "tool", help = "Annotation Jar for builder invocations.")
        public Path annotationJar;

        @Option(name = "androidJar", defaultValue = "null", converter = ExistingPathConverter.class, category = "tool", help = "Path to the android jar for resource packaging and building apks.")
        public Path androidJar;

        @Option(name = "useAaptCruncher", defaultValue = "auto", category = "config", help = "Use the legacy aapt cruncher, defaults to true for non-LIBRARY packageTypes. "
                + " LIBRARY packages do not benefit from the additional processing as the resources"
                + " will need to be reprocessed during the generation of the final apk. See"
                + " https://code.google.com/p/android/issues/detail?id=67525 for a discussion of the"
                + " different png crunching methods.")
        public TriState useAaptCruncher;

        @Option(name = "uncompressedExtensions", defaultValue = "", converter = CommaSeparatedOptionListConverter.class, category = "config", help = "A list of file extensions not to compress.")
        public List<String> uncompressedExtensions;

        @Option(name = "assetsToIgnore", defaultValue = "", converter = CommaSeparatedOptionListConverter.class, category = "config", help = "A list of assets extensions to ignore.")
        public List<String> assetsToIgnore;

        @Option(name = "debug", defaultValue = "false", category = "config", help = "Indicates if it is a debug build.")
        public boolean debug;

        @Option(name = "resourceConfigs", defaultValue = "", converter = CommaSeparatedOptionListConverter.class, category = "config", help = "A list of resource config filters to pass to aapt.")
        public List<String> resourceConfigs;

        private static final String ANDROID_SPLIT_DOCUMENTATION_URL = "https://developer.android.com/guide/topics/resources/providing-resources.html"
                + "#QualifierRules";

        @Option(name = "split", defaultValue = "required but ignored due to allowMultiple", category = "config", allowMultiple = true, help = "An individual split configuration to pass to aapt."
                + " Each split is a list of configuration filters separated by commas."
                + " Configuration filters are lists of configuration qualifiers separated by dashes,"
                + " as used in resource directory names and described on the Android developer site: "
                + ANDROID_SPLIT_DOCUMENTATION_URL
                + " For example, a split might be 'en-television,en-xxhdpi', containing English"
                + " assets which either are for TV screens or are extra extra high resolution."
                + " Multiple splits can be specified by passing this flag multiple times."
                + " Each split flag will produce an additional output file, named by replacing the"
                + " commas in the split specification with underscores, and appending the result to"
                + " the output package name following an underscore.")
        public List<String> splits;
    }

    /**
     * {@link AaptOptions} backed by an {@link AaptConfigOptions}.
     */
    public static final class FlagAaptOptions implements AaptOptions {
        private final AaptConfigOptions options;

        public FlagAaptOptions(AaptConfigOptions options) {
            this.options = options;
        }

        @Override
        public Collection<String> getNoCompress() {
            if (!options.uncompressedExtensions.isEmpty()) {
                return options.uncompressedExtensions;
            }
            return ImmutableList.of();
        }

        @Override
        public String getIgnoreAssets() {
            if (!options.assetsToIgnore.isEmpty()) {
                return Joiner.on(":").join(options.assetsToIgnore);
            }
            return null;
        }

        @Override
        public boolean getFailOnMissingConfigEntry() {
            return false;
        }

        @Override
        public List<String> getAdditionalParameters() {
            return ImmutableList.of();
        }
    }

    /** Shutdowns and verifies that no tasks are running in the executor service. */
    private static final class ExecutorServiceCloser implements Closeable {
        private final ListeningExecutorService executorService;

        private ExecutorServiceCloser(ListeningExecutorService executorService) {
            this.executorService = executorService;
        }

        @Override
        public void close() throws IOException {
            List<Runnable> unfinishedTasks = executorService.shutdownNow();
            if (!unfinishedTasks.isEmpty()) {
                throw new IOException("Shutting down the executor with unfinished tasks:" + unfinishedTasks);
            }
        }

        public static Closeable createWith(ListeningExecutorService executorService) {
            return new ExecutorServiceCloser(executorService);
        }
    }

    private static final ImmutableMap<SystemProperty, String> SYSTEM_PROPERTY_NAMES = Maps
            .toMap(Arrays.asList(SystemProperty.values()), new Function<SystemProperty, String>() {
                @Override
                public String apply(SystemProperty property) {
                    if (property == SystemProperty.PACKAGE) {
                        return "applicationId";
                    } else {
                        return property.toCamelCase();
                    }
                }
            });

    private static final Pattern HEX_REGEX = Pattern.compile("0x[0-9A-Fa-f]{8}");
    private final StdLogger stdLogger;

    public AndroidResourceProcessor(StdLogger stdLogger) {
        this.stdLogger = stdLogger;
    }

    /**
     * Copies the R.txt to the expected place.
     *
     * @param generatedSourceRoot The path to the generated R.txt.
     * @param rOutput The Path to write the R.txt.
     * @param staticIds Boolean that indicates if the ids should be set to 0x1 for caching purposes.
     */
    public void copyRToOutput(Path generatedSourceRoot, Path rOutput, boolean staticIds) {
        try {
            Files.createDirectories(rOutput.getParent());
            final Path source = generatedSourceRoot.resolve("R.txt");
            if (Files.exists(source)) {
                if (staticIds) {
                    String contents = HEX_REGEX.matcher(Joiner.on("\n").join(Files.readAllLines(source, UTF_8)))
                            .replaceAll("0x1");
                    Files.write(rOutput, contents.getBytes(UTF_8));
                } else {
                    Files.copy(source, rOutput);
                }
            } else {
                // The R.txt wasn't generated, create one for future inheritance, as Bazel always requires
                // outputs. This state occurs when there are no resource directories.
                Files.createFile(rOutput);
            }
            // Set to the epoch for caching purposes.
            Files.setLastModifiedTime(rOutput, FileTime.fromMillis(0L));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Creates a zip archive from all found R.java files.
     */
    public void createSrcJar(Path generatedSourcesRoot, Path srcJar, boolean staticIds) {
        try {
            Files.createDirectories(srcJar.getParent());
            try (final ZipOutputStream zip = new ZipOutputStream(
                    new BufferedOutputStream(Files.newOutputStream(srcJar)))) {
                SymbolFileSrcJarBuildingVisitor visitor = new SymbolFileSrcJarBuildingVisitor(zip,
                        generatedSourcesRoot, staticIds);
                Files.walkFileTree(generatedSourcesRoot, visitor);
            }
            // Set to the epoch for caching purposes.
            Files.setLastModifiedTime(srcJar, FileTime.fromMillis(0L));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Creates a zip archive from all found R.class (and inner class) files.
     */
    public void createClassJar(Path generatedClassesRoot, Path classJar) {
        try {
            Files.createDirectories(classJar.getParent());
            try (final ZipOutputStream zip = new ZipOutputStream(
                    new BufferedOutputStream(Files.newOutputStream(classJar)))) {
                ClassJarBuildingVisitor visitor = new ClassJarBuildingVisitor(zip, generatedClassesRoot);
                Files.walkFileTree(generatedClassesRoot, visitor);
                visitor.writeManifestContent();
            }
            // Set to the epoch for caching purposes.
            Files.setLastModifiedTime(classJar, FileTime.fromMillis(0L));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Copies the AndroidManifest.xml to the specified output location.
     *
     * @param androidData The MergedAndroidData which contains the manifest to be written to
     *    manifestOut.
     * @param manifestOut The Path to write the AndroidManifest.xml.
     */
    public void copyManifestToOutput(MergedAndroidData androidData, Path manifestOut) {
        try {
            Files.createDirectories(manifestOut.getParent());
            Files.copy(androidData.getManifest(), manifestOut);
            // Set to the epoch for caching purposes.
            Files.setLastModifiedTime(manifestOut, FileTime.fromMillis(0L));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Creates a zip file containing the provided android resources and assets.
     *
     * @param resourcesRoot The root containing android resources to be written.
     * @param assetsRoot The root containing android assets to be written.
     * @param output The path to write the zip file
     * @param compress Whether or not to compress the content
     * @throws IOException
     */
    public void createResourcesZip(Path resourcesRoot, Path assetsRoot, Path output, boolean compress)
            throws IOException {
        try (ZipOutputStream zout = new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(output)))) {
            if (Files.exists(resourcesRoot)) {
                ZipBuilderVisitor visitor = new ZipBuilderVisitor(zout, resourcesRoot, "res");
                visitor.setCompress(compress);
                Files.walkFileTree(resourcesRoot, visitor);
            }
            if (Files.exists(assetsRoot)) {
                ZipBuilderVisitor visitor = new ZipBuilderVisitor(zout, assetsRoot, "assets");
                visitor.setCompress(compress);
                Files.walkFileTree(assetsRoot, visitor);
            }
        }
    }

    // TODO(bazel-team): Clean up this method call -- 13 params is too many.
    /** Processes resources for generated sources, configs and packaging resources. */
    public void processResources(Path aapt, Path androidJar, @Nullable Revision buildToolsVersion,
            VariantType variantType, boolean debug, String customPackageForR, AaptOptions aaptOptions,
            Collection<String> resourceConfigs, Collection<String> splits, MergedAndroidData primaryData,
            List<DependencyAndroidData> dependencyData, @Nullable Path sourceOut, @Nullable Path packageOut,
            @Nullable Path proguardOut, @Nullable Path mainDexProguardOut, @Nullable Path publicResourcesOut,
            @Nullable Path dataBindingInfoOut)
            throws IOException, InterruptedException, LoggedErrorException, UnrecognizedSplitsException {
        Path androidManifest = primaryData.getManifest();
        final Path resourceDir = processDataBindings(primaryData.getResourceDir(), dataBindingInfoOut, variantType,
                customPackageForR, androidManifest);

        final Path assetsDir = primaryData.getAssetDir();
        if (publicResourcesOut != null) {
            prepareOutputPath(publicResourcesOut.getParent());
        }
        runAapt(aapt, androidJar, buildToolsVersion, variantType, debug, customPackageForR, aaptOptions,
                resourceConfigs, splits, androidManifest, resourceDir, assetsDir, sourceOut, packageOut,
                proguardOut, mainDexProguardOut, publicResourcesOut);
        // The R needs to be created for each library in the dependencies,
        // but only if the current project is not a library.
        if (sourceOut != null && variantType != VariantType.LIBRARY) {
            writeDependencyPackageRJavaFiles(dependencyData, customPackageForR, androidManifest, sourceOut);
        }
        // Reset the output date stamps.
        if (proguardOut != null) {
            Files.setLastModifiedTime(proguardOut, FileTime.fromMillis(0L));
        }
        if (mainDexProguardOut != null) {
            Files.setLastModifiedTime(mainDexProguardOut, FileTime.fromMillis(0L));
        }
        if (packageOut != null) {
            Files.setLastModifiedTime(packageOut, FileTime.fromMillis(0L));
            if (!splits.isEmpty()) {
                Iterable<Path> splitFilenames = findAndRenameSplitPackages(packageOut, splits);
                for (Path splitFilename : splitFilenames) {
                    Files.setLastModifiedTime(splitFilename, FileTime.fromMillis(0L));
                }
            }
        }
        if (publicResourcesOut != null && Files.exists(publicResourcesOut)) {
            Files.setLastModifiedTime(publicResourcesOut, FileTime.fromMillis(0L));
        }
    }

    public void runAapt(Path aapt, Path androidJar, @Nullable Revision buildToolsVersion, VariantType variantType,
            boolean debug, String customPackageForR, AaptOptions aaptOptions, Collection<String> resourceConfigs,
            Collection<String> splits, Path androidManifest, Path resourceDir, Path assetsDir, Path sourceOut,
            @Nullable Path packageOut, @Nullable Path proguardOut, @Nullable Path mainDexProguardOut,
            @Nullable Path publicResourcesOut) throws InterruptedException, LoggedErrorException, IOException {
        AaptCommandBuilder commandBuilder = new AaptCommandBuilder(aapt).forBuildToolsVersion(buildToolsVersion)
                .forVariantType(variantType)
                // first argument is the command to be executed, "package"
                .add("package")
                // If the logger is verbose, set aapt to be verbose
                .when(stdLogger.getLevel() == StdLogger.Level.VERBOSE).thenAdd("-v")
                // Overwrite existing files, if they exist.
                .add("-f")
                // Resources are precrunched in the merge process.
                .add("--no-crunch")
                // Do not automatically generate versioned copies of vector XML resources.
                .whenVersionIsAtLeast(new Revision(23)).thenAdd("--no-version-vectors")
                // Add the android.jar as a base input.
                .add("-I", androidJar)
                // Add the manifest for validation.
                .add("-M", androidManifest.toAbsolutePath())
                // Maybe add the resources if they exist
                .when(Files.isDirectory(resourceDir)).thenAdd("-S", resourceDir)
                // Maybe add the assets if they exist
                .when(Files.isDirectory(assetsDir)).thenAdd("-A", assetsDir)
                // Outputs
                .when(sourceOut != null).thenAdd("-m").add("-J", prepareOutputPath(sourceOut))
                .add("--output-text-symbols", prepareOutputPath(sourceOut)).add("-F", packageOut)
                .add("-G", proguardOut).whenVersionIsAtLeast(new Revision(24)).thenAdd("-D", mainDexProguardOut)
                .add("-P", publicResourcesOut).when(debug).thenAdd("--debug-mode")
                .add("--custom-package", customPackageForR)
                // If it is a library, do not generate final java ids.
                .whenVariantIs(VariantType.LIBRARY).thenAdd("--non-constant-id")
                .add("--ignore-assets", aaptOptions.getIgnoreAssets())
                .when(aaptOptions.getFailOnMissingConfigEntry()).thenAdd("--error-on-missing-config-entry")
                // Never compress apks.
                .add("-0", "apk")
                // Add custom no-compress extensions.
                .addRepeated("-0", aaptOptions.getNoCompress())
                // Filter by resource configuration type.
                .add("-c", Joiner.on(',').join(resourceConfigs))
                // Split APKs if any splits were specified.
                .whenVersionIsAtLeast(new Revision(23)).thenAddRepeated("--split", splits);
        try {
            new CommandLineRunner(stdLogger).runCmdLine(commandBuilder.build(), null);
        } catch (LoggedErrorException e) {
            // Add context and throw the error to resume processing.
            throw new LoggedErrorException(e.getCmdLineError(), getOutputWithSourceContext(aapt, e.getOutput()),
                    e.getCmdLine());
        }
    }

    /** Adds 10 lines of source to each syntax error. Very useful for debugging. */
    private List<String> getOutputWithSourceContext(Path aapt, List<String> lines) throws IOException {
        List<String> outputWithSourceContext = new ArrayList<>();
        for (String line : lines) {
            if (line.contains("Duplicate file") || line.contains("Original")) {
                String[] parts = line.split(":");
                String fileName = parts[0].trim();
                outputWithSourceContext.add("\n" + fileName + ":\n\t");
                outputWithSourceContext.add(Joiner.on("\n\t")
                        .join(Files.readAllLines(aapt.getFileSystem().getPath(fileName), StandardCharsets.UTF_8)));
            } else if (line.contains("error")) {
                String[] parts = line.split(":");
                String fileName = parts[0].trim();
                try {
                    int lineNumber = Integer.valueOf(parts[1].trim());
                    StringBuilder expandedError = new StringBuilder("\nError at " + lineNumber + " : " + line);
                    List<String> errorSource = Files.readAllLines(aapt.getFileSystem().getPath(fileName),
                            StandardCharsets.UTF_8);
                    for (int i = Math.max(lineNumber - 5, 0); i < Math.min(lineNumber + 5,
                            errorSource.size()); i++) {
                        expandedError.append("\n").append(i).append("\t:  ").append(errorSource.get(i));
                    }
                    outputWithSourceContext.add(expandedError.toString());
                } catch (IOException | NumberFormatException formatError) {
                    outputWithSourceContext.add("error parsing line" + line);
                    stdLogger.error(formatError, "error during reading source %s", fileName);
                }
            } else {
                outputWithSourceContext.add(line);
            }
        }
        return outputWithSourceContext;
    }

    /**
     * If resources exist and a data binding layout info file is requested: processes data binding
     * declarations over those resources, populates the output file, and creates a new resources
     * directory with data binding expressions stripped out (so aapt, which doesn't understand
     * data binding, can properly read them).
     *
     * <p>Returns the resources directory that aapt should read.
     */
    private Path processDataBindings(Path resourceDir, Path dataBindingInfoOut, VariantType variantType,
            String packagePath, Path androidManifest) throws IOException {

        if (dataBindingInfoOut == null) {
            return resourceDir;
        } else if (!Files.isDirectory(resourceDir)) {
            // No resources: no data binding needed. Create a dummy file to satisfy declared outputs.
            Files.createFile(dataBindingInfoOut);
            return resourceDir;
        }

        // Strip the file name (the data binding library automatically adds it back in).
        // ** The data binding library assumes this file is called "layout-info.zip". **
        dataBindingInfoOut = dataBindingInfoOut.getParent();
        if (Files.notExists(dataBindingInfoOut)) {
            Files.createDirectory(dataBindingInfoOut);
        }

        Path processedResourceDir = resourceDir.resolveSibling("res_without_databindings");
        if (Files.notExists(processedResourceDir)) {
            Files.createDirectory(processedResourceDir);
        }

        ProcessXmlOptions options = new ProcessXmlOptions();
        options.setAppId(packagePath);
        options.setLibrary(variantType == VariantType.LIBRARY);
        options.setResInput(resourceDir.toFile());
        options.setResOutput(processedResourceDir.toFile());
        options.setLayoutInfoOutput(dataBindingInfoOut.toFile());
        options.setZipLayoutInfo(true); // Aggregate data-bound .xml files into a single .zip.

        try {
            Object minSdk = AndroidManifest.getMinSdkVersion(new FileWrapper(androidManifest.toFile()));
            if (minSdk instanceof Integer) {
                options.setMinSdk(((Integer) minSdk).intValue());
            } else {
                // TODO(bazel-team): Enforce the minimum SDK check.
                options.setMinSdk(15);
            }
        } catch (XPathExpressionException | StreamException e) {
            // TODO(bazel-team): Enforce the minimum SDK check.
            options.setMinSdk(15);
        }

        try {
            AndroidDataBinding.doRun(options);
        } catch (Throwable t) {
            throw new RuntimeException(t);
        }
        return processedResourceDir;
    }

    /** Task to parse java package from AndroidManifest.xml */
    private static final class PackageParsingTask implements Callable<String> {

        private final File manifest;

        PackageParsingTask(File manifest) {
            this.manifest = manifest;
        }

        @Override
        public String call() throws Exception {
            return VariantConfiguration.getManifestPackage(manifest);
        }
    }

    /** Task to load and parse R.txt symbols */
    private static final class SymbolLoadingTask implements Callable<Object> {

        private final SymbolLoader symbolLoader;

        SymbolLoadingTask(SymbolLoader symbolLoader) {
            this.symbolLoader = symbolLoader;
        }

        @Override
        public Object call() throws Exception {
            symbolLoader.load();
            return null;
        }
    }

    @Nullable
    public SymbolLoader loadResourceSymbolTable(List<SymbolFileProvider> libraries, String appPackageName,
            Path primaryRTxt, Multimap<String, SymbolLoader> libMap) throws IOException {
        // The reported availableProcessors may be higher than the actual resources
        // (on a shared system). On the other hand, a lot of the work is I/O, so it's not completely
        // CPU bound. As a compromise, divide by 2 the reported availableProcessors.
        int numThreads = Math.max(1, Runtime.getRuntime().availableProcessors() / 2);
        ListeningExecutorService executorService = MoreExecutors
                .listeningDecorator(Executors.newFixedThreadPool(numThreads));
        try (Closeable closeable = ExecutorServiceCloser.createWith(executorService)) {
            // Load the package names from the manifest files.
            Map<SymbolFileProvider, ListenableFuture<String>> packageJobs = new HashMap<>();
            for (final SymbolFileProvider lib : libraries) {
                packageJobs.put(lib, executorService.submit(new PackageParsingTask(lib.getManifest())));
            }
            Map<SymbolFileProvider, String> packageNames = new HashMap<>();
            try {
                for (Map.Entry<SymbolFileProvider, ListenableFuture<String>> entry : packageJobs.entrySet()) {
                    packageNames.put(entry.getKey(), entry.getValue().get());
                }
            } catch (InterruptedException | ExecutionException e) {
                throw new IOException("Failed to load package name: ", e);
            }
            // Associate the packages with symbol files.
            for (SymbolFileProvider lib : libraries) {
                String packageName = packageNames.get(lib);
                // If the library package matches the app package skip -- the final app resource IDs are
                // stored in the primaryRTxt file.
                if (appPackageName.equals(packageName)) {
                    continue;
                }
                File rFile = lib.getSymbolFile();
                // If the library has no resource, this file won't exist.
                if (rFile.isFile()) {
                    SymbolLoader libSymbols = new SymbolLoader(rFile, stdLogger);
                    libMap.put(packageName, libSymbols);
                }
            }
            // Even if there are no libraries, load fullSymbolValues, in case we only have resources
            // defined for the binary.
            File primaryRTxtFile = primaryRTxt.toFile();
            SymbolLoader fullSymbolValues = null;
            if (primaryRTxtFile.isFile()) {
                fullSymbolValues = new SymbolLoader(primaryRTxtFile, stdLogger);
            }
            // Now load the symbol files in parallel.
            List<ListenableFuture<?>> loadJobs = new ArrayList<>();
            Iterable<SymbolLoader> toLoad = fullSymbolValues != null
                    ? Iterables.concat(libMap.values(), ImmutableList.of(fullSymbolValues))
                    : libMap.values();
            for (final SymbolLoader loader : toLoad) {
                loadJobs.add(executorService.submit(new SymbolLoadingTask(loader)));
            }
            try {
                Futures.allAsList(loadJobs).get();
            } catch (InterruptedException | ExecutionException e) {
                throw new IOException("Failed to load SymbolFile: ", e);
            }
            return fullSymbolValues;
        }
    }

    void writeDependencyPackageRJavaFiles(List<DependencyAndroidData> dependencyData, String customPackageForR,
            Path androidManifest, Path sourceOut) throws IOException {
        List<SymbolFileProvider> libraries = new ArrayList<>();
        for (DependencyAndroidData dataDep : dependencyData) {
            SymbolFileProvider library = dataDep.asSymbolFileProvider();
            libraries.add(library);
        }
        String appPackageName = customPackageForR;
        if (appPackageName == null) {
            appPackageName = VariantConfiguration.getManifestPackage(androidManifest.toFile());
        }
        Multimap<String, SymbolLoader> libSymbolMap = ArrayListMultimap.create();
        Path primaryRTxt = sourceOut != null ? sourceOut.resolve("R.txt") : null;
        if (primaryRTxt != null && !libraries.isEmpty()) {
            SymbolLoader fullSymbolValues = loadResourceSymbolTable(libraries, appPackageName, primaryRTxt,
                    libSymbolMap);
            if (fullSymbolValues != null) {
                writePackageRJavaFiles(libSymbolMap, fullSymbolValues, sourceOut);
            }
        }
    }

    private void writePackageRJavaFiles(Multimap<String, SymbolLoader> libMap, SymbolLoader fullSymbolValues,
            Path sourceOut) throws IOException {
        // Loop on all the package name, merge all the symbols to write, and write.
        for (String packageName : libMap.keySet()) {
            Collection<SymbolLoader> symbols = libMap.get(packageName);
            SymbolWriter writer = new SymbolWriter(sourceOut.toString(), packageName, fullSymbolValues);
            for (SymbolLoader symbolLoader : symbols) {
                writer.addSymbolsToWrite(symbolLoader);
            }
            writer.write();
        }
    }

    void writePackageRClasses(Multimap<String, SymbolLoader> libMap, SymbolLoader fullSymbolValues,
            String appPackageName, Path classesOut, boolean finalFields) throws IOException {
        for (String packageName : libMap.keySet()) {
            Collection<SymbolLoader> symbols = libMap.get(packageName);
            RClassGenerator classWriter = RClassGenerator.fromSymbols(classesOut, packageName, fullSymbolValues,
                    symbols, finalFields);
            classWriter.write();
        }
        // Unlike the R.java generation, we also write the app's R.class file so that the class
        // jar file can be complete (aapt doesn't generate it for us).
        RClassGenerator classWriter = RClassGenerator.fromSymbols(classesOut, appPackageName, fullSymbolValues,
                ImmutableList.of(fullSymbolValues), finalFields);
        classWriter.write();
    }

    /** Finds aapt's split outputs and renames them according to the input flags. */
    private Iterable<Path> findAndRenameSplitPackages(Path packageOut, Iterable<String> splits)
            throws UnrecognizedSplitsException, IOException {
        String prefix = packageOut.getFileName().toString() + "_";
        // The regex java string literal below is received as [\\{}\[\]*?] by the regex engine,
        // which produces a character class containing \{}[]*?
        // The replacement string literal is received as \\$0 by the regex engine, which places
        // a backslash before the match.
        String prefixGlob = prefix.replaceAll("[\\\\{}\\[\\]*?]", "\\\\$0") + "*";
        Path outputDirectory = packageOut.getParent();
        ImmutableList.Builder<String> filenameSuffixes = new ImmutableList.Builder<>();
        try (DirectoryStream<Path> glob = Files.newDirectoryStream(outputDirectory, prefixGlob)) {
            for (Path file : glob) {
                filenameSuffixes.add(file.getFileName().toString().substring(prefix.length()));
            }
        }
        Map<String, String> outputs = SplitConfigurationFilter.mapFilenamesToSplitFlags(filenameSuffixes.build(),
                splits);
        ImmutableList.Builder<Path> outputPaths = new ImmutableList.Builder<>();
        for (Map.Entry<String, String> splitMapping : outputs.entrySet()) {
            Path resultPath = packageOut.resolveSibling(prefix + splitMapping.getValue());
            outputPaths.add(resultPath);
            if (!splitMapping.getKey().equals(splitMapping.getValue())) {
                Path sourcePath = packageOut.resolveSibling(prefix + splitMapping.getKey());
                Files.move(sourcePath, resultPath);
            }
        }
        return outputPaths.build();
    }

    public MergedAndroidData processManifest(VariantType variantType, String customPackageForR,
            String applicationId, int versionCode, String versionName, MergedAndroidData primaryData,
            Path processedManifest) throws IOException {

        ManifestMerger2.MergeType mergeType = variantType == VariantType.DEFAULT
                ? ManifestMerger2.MergeType.APPLICATION
                : ManifestMerger2.MergeType.LIBRARY;

        String newManifestPackage = variantType == VariantType.DEFAULT ? applicationId : customPackageForR;

        if (versionCode != -1 || versionName != null || newManifestPackage != null) {
            Files.createDirectories(processedManifest.getParent());

            // The generics on Invoker don't make sense, so ignore them.
            @SuppressWarnings("unchecked")
            Invoker<?> manifestMergerInvoker = ManifestMerger2.newMerger(primaryData.getManifest().toFile(),
                    stdLogger, mergeType);
            // Stamp new package
            if (newManifestPackage != null) {
                manifestMergerInvoker.setOverride(SystemProperty.PACKAGE, newManifestPackage);
            }
            // Stamp version and applicationId (if provided) into the manifest
            if (versionCode > 0) {
                manifestMergerInvoker.setOverride(SystemProperty.VERSION_CODE, String.valueOf(versionCode));
            }
            if (versionName != null) {
                manifestMergerInvoker.setOverride(SystemProperty.VERSION_NAME, versionName);
            }

            MergedManifestKind mergedManifestKind = MergedManifestKind.MERGED;
            if (mergeType == ManifestMerger2.MergeType.APPLICATION) {
                manifestMergerInvoker.withFeatures(Invoker.Feature.REMOVE_TOOLS_DECLARATIONS);
            }

            try {
                MergingReport mergingReport = manifestMergerInvoker.merge();
                switch (mergingReport.getResult()) {
                case WARNING:
                    mergingReport.log(stdLogger);
                    writeMergedManifest(mergedManifestKind, mergingReport, processedManifest);
                    break;
                case SUCCESS:
                    writeMergedManifest(mergedManifestKind, mergingReport, processedManifest);
                    break;
                case ERROR:
                    mergingReport.log(stdLogger);
                    throw new RuntimeException(mergingReport.getReportString());
                default:
                    throw new RuntimeException("Unhandled result type : " + mergingReport.getResult());
                }
            } catch (IOException | MergeFailureException e) {
                throw new RuntimeException(e);
            }
            return new MergedAndroidData(primaryData.getResourceDir(), primaryData.getAssetDir(),
                    processedManifest);
        }
        return primaryData;
    }

    /**
     * A logger that will print messages to a target OutputStream.
     */
    private static final class PrintStreamLogger implements ILogger {
        private final PrintStream out;

        public PrintStreamLogger(PrintStream stream) {
            this.out = stream;
        }

        @Override
        public void error(@Nullable Throwable t, @Nullable String msgFormat, Object... args) {
            if (msgFormat != null) {
                out.println(String.format("Error: " + msgFormat, args));
            }
            if (t != null) {
                out.printf("Error: %s%n", t.getMessage());
            }
        }

        @Override
        public void warning(@NonNull String msgFormat, Object... args) {
            out.println(String.format("Warning: " + msgFormat, args));
        }

        @Override
        public void info(@NonNull String msgFormat, Object... args) {
            out.println(String.format("Info: " + msgFormat, args));
        }

        @Override
        public void verbose(@NonNull String msgFormat, Object... args) {
            out.println(String.format(msgFormat, args));
        }
    }

    /**
     * Merge several manifests into one and perform placeholder substitutions. This operation uses
     * Gradle semantics.
     *
     * @param manifest The primary manifest of the merge.
     * @param mergeeManifests Manifests to be merged into {@code manifest}.
     * @param mergeType Whether the merger should operate in application or library mode.
     * @param values A map of strings to be used as manifest placeholders and overrides. packageName
     *     is the only disallowed value and will be ignored.
     * @param output The path to write the resultant manifest to.
     * @param logFile The path to write the merger log to.
     * @return The path of the resultant manifest, either {@code output}, or {@code manifest} if no
     *     merging was required.
     * @throws IOException if there was a problem writing the merged manifest.
     */
    public Path mergeManifest(Path manifest, Map<Path, String> mergeeManifests, MergeType mergeType,
            Map<String, String> values, Path output, Path logFile) throws IOException {
        if (mergeeManifests.isEmpty() && values.isEmpty()) {
            return manifest;
        }

        Invoker<?> manifestMerger = ManifestMerger2.newMerger(manifest.toFile(), stdLogger, mergeType);
        MergedManifestKind mergedManifestKind = MergedManifestKind.MERGED;
        if (mergeType == MergeType.APPLICATION) {
            manifestMerger.withFeatures(Feature.REMOVE_TOOLS_DECLARATIONS);
        }

        // Add mergee manifests
        List<Pair<String, File>> libraryManifests = new ArrayList<>();
        for (Entry<Path, String> mergeeManifest : mergeeManifests.entrySet()) {
            libraryManifests.add(Pair.of(mergeeManifest.getValue(), mergeeManifest.getKey().toFile()));
        }
        manifestMerger.addLibraryManifests(libraryManifests);

        // Extract SystemProperties from the provided values.
        Map<String, Object> placeholders = new HashMap<>();
        placeholders.putAll(values);
        for (SystemProperty property : SystemProperty.values()) {
            if (values.containsKey(SYSTEM_PROPERTY_NAMES.get(property))) {
                manifestMerger.setOverride(property, values.get(SYSTEM_PROPERTY_NAMES.get(property)));

                // The manifest merger does not allow explicitly specifying either applicationId or
                // packageName as placeholders if SystemProperty.PACKAGE is specified. It forces these
                // placeholders to have the same value as specified by SystemProperty.PACKAGE.
                if (property == SystemProperty.PACKAGE) {
                    placeholders.remove(PlaceholderHandler.APPLICATION_ID);
                    placeholders.remove(PlaceholderHandler.PACKAGE_NAME);
                }
            }
        }

        // Add placeholders for all values.
        // packageName is populated from either the applicationId override or from the manifest itself;
        // it cannot be manually specified.
        placeholders.remove(PlaceholderHandler.PACKAGE_NAME);
        manifestMerger.setPlaceHolderValues(placeholders);

        try {
            MergingReport mergingReport = manifestMerger.merge();

            if (logFile != null) {
                logFile.getParent().toFile().mkdirs();
                try (PrintStream stream = new PrintStream(logFile.toFile())) {
                    mergingReport.log(new PrintStreamLogger(stream));
                }
            }
            switch (mergingReport.getResult()) {
            case WARNING:
                mergingReport.log(stdLogger);
                Files.createDirectories(output.getParent());
                writeMergedManifest(mergedManifestKind, mergingReport, output);
                break;
            case SUCCESS:
                Files.createDirectories(output.getParent());
                writeMergedManifest(mergedManifestKind, mergingReport, output);
                break;
            case ERROR:
                mergingReport.log(stdLogger);
                throw new RuntimeException(mergingReport.getReportString());
            default:
                throw new RuntimeException("Unhandled result type : " + mergingReport.getResult());
            }
        } catch (MergeFailureException e) {
            throw new RuntimeException(e);
        }

        return output;
    }

    private void writeMergedManifest(MergedManifestKind mergedManifestKind, MergingReport mergingReport,
            Path manifestOut) throws IOException {
        String manifestContents = mergingReport.getMergedDocument(mergedManifestKind);
        String annotatedDocument = mergingReport.getMergedDocument(MergedManifestKind.BLAME);
        stdLogger.verbose(annotatedDocument);
        Files.write(manifestOut, manifestContents.getBytes(UTF_8));
    }

    public void writeDummyManifestForAapt(Path dummyManifest, String packageForR) throws IOException {
        Files.createDirectories(dummyManifest.getParent());
        Files.write(dummyManifest,
                String.format("<?xml version=\"1.0\" encoding=\"utf-8\"?>"
                        + "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\""
                        + " package=\"%s\">" + "</manifest>", packageForR).getBytes(UTF_8));
    }

    /**
     * Overwrite the package attribute of {@code <manifest>} in an AndroidManifest.xml file.
     *
     * @param manifest The input manifest.
     * @param customPackage The package to write to the manifest.
     * @param output The output manifest to generate.
     * @return The output manifest if generated or the input manifest if no overwriting is required.
     */
    /* TODO(apell): switch from custom xml parsing to Gradle merger with NO_PLACEHOLDER_REPLACEMENT
     * set when android common is updated to version 2.5.0.
     */
    public Path writeManifestPackage(Path manifest, String customPackage, Path output) {
        if (Strings.isNullOrEmpty(customPackage)) {
            return manifest;
        }
        try {
            Files.createDirectories(output.getParent());
            XMLEventReader reader = XMLInputFactory.newInstance()
                    .createXMLEventReader(Files.newInputStream(manifest), UTF_8.name());
            XMLEventWriter writer = XMLOutputFactory.newInstance()
                    .createXMLEventWriter(Files.newOutputStream(output), UTF_8.name());
            XMLEventFactory eventFactory = XMLEventFactory.newInstance();
            while (reader.hasNext()) {
                XMLEvent event = reader.nextEvent();
                if (event.isStartElement()
                        && event.asStartElement().getName().toString().equalsIgnoreCase("manifest")) {
                    StartElement element = event.asStartElement();
                    @SuppressWarnings("unchecked")
                    Iterator<Attribute> attributes = element.getAttributes();
                    ImmutableList.Builder<Attribute> newAttributes = ImmutableList.builder();
                    while (attributes.hasNext()) {
                        Attribute attr = attributes.next();
                        if (attr.getName().toString().equalsIgnoreCase("package")) {
                            newAttributes.add(eventFactory.createAttribute("package", customPackage));
                        } else {
                            newAttributes.add(attr);
                        }
                    }
                    writer.add(eventFactory.createStartElement(element.getName(), newAttributes.build().iterator(),
                            element.getNamespaces()));
                } else {
                    writer.add(event);
                }
            }
            writer.flush();
        } catch (XMLStreamException | FactoryConfigurationError | IOException e) {
            throw new RuntimeException(e);
        }

        return output;
    }

    /**
     * Merges all secondary resources with the primary resources, given that the primary resources
     * have not yet been parsed and serialized.
     */
    public MergedAndroidData mergeData(final UnvalidatedAndroidData primary,
            final List<? extends SerializedAndroidData> direct,
            final List<? extends SerializedAndroidData> transitive, final Path resourcesOut, final Path assetsOut,
            @Nullable final PngCruncher cruncher, final VariantType type, @Nullable final Path symbolsOut)
            throws MergingException {
        try {
            final ParsedAndroidData parsedPrimary = ParsedAndroidData.from(primary);
            return mergeData(parsedPrimary, primary.getManifest(), direct, transitive, resourcesOut, assetsOut,
                    cruncher, type, symbolsOut, null /* rclassWriter */);
        } catch (IOException e) {
            throw MergingException.wrapException(e).build();
        }
    }

    /**
     * Merges all secondary resources with the primary resources, given that the primary resources
     * have been separately parsed and serialized.
     */
    public MergedAndroidData mergeData(final SerializedAndroidData primary, final Path primaryManifest,
            final List<? extends SerializedAndroidData> direct,
            final List<? extends SerializedAndroidData> transitive, final Path resourcesOut, final Path assetsOut,
            @Nullable final PngCruncher cruncher, final VariantType type, @Nullable final Path symbolsOut,
            @Nullable final AndroidResourceClassWriter rclassWriter) throws MergingException {
        final ParsedAndroidData.Builder primaryBuilder = ParsedAndroidData.Builder.newBuilder();
        final AndroidDataSerializer serializer = AndroidDataSerializer.create();
        primary.deserialize(serializer, primaryBuilder.consumers());
        ParsedAndroidData primaryData = primaryBuilder.build();
        return mergeData(primaryData, primaryManifest, direct, transitive, resourcesOut, assetsOut, cruncher, type,
                symbolsOut, rclassWriter);
    }

    /**
     * Merges all secondary resources with the primary resources.
     */
    private MergedAndroidData mergeData(final ParsedAndroidData primary, final Path primaryManifest,
            final List<? extends SerializedAndroidData> direct,
            final List<? extends SerializedAndroidData> transitive, final Path resourcesOut, final Path assetsOut,
            @Nullable final PngCruncher cruncher, final VariantType type, @Nullable final Path symbolsOut,
            @Nullable AndroidResourceClassWriter rclassWriter) throws MergingException {
        Stopwatch timer = Stopwatch.createStarted();
        final ListeningExecutorService executorService = MoreExecutors
                .listeningDecorator(Executors.newFixedThreadPool(15));
        try (Closeable closeable = ExecutorServiceCloser.createWith(executorService)) {
            AndroidDataMerger merger = AndroidDataMerger.createWithPathDeduplictor(executorService);
            UnwrittenMergedAndroidData merged = merger.loadAndMerge(transitive, direct, primary, primaryManifest,
                    type != VariantType.LIBRARY);
            logger.fine(String.format("merge finished in %sms", timer.elapsed(TimeUnit.MILLISECONDS)));
            timer.reset().start();
            if (symbolsOut != null) {
                AndroidDataSerializer serializer = AndroidDataSerializer.create();
                merged.serializeTo(serializer);
                serializer.flushTo(symbolsOut);
                logger.fine(
                        String.format("serialize merge finished in %sms", timer.elapsed(TimeUnit.MILLISECONDS)));
                timer.reset().start();
            }
            if (rclassWriter != null) {
                merged.writeResourceClass(rclassWriter);
                logger.fine(String.format("write classes finished in %sms", timer.elapsed(TimeUnit.MILLISECONDS)));
                timer.reset().start();
            }
            AndroidDataWriter writer = AndroidDataWriter.createWith(resourcesOut.getParent(), resourcesOut,
                    assetsOut, cruncher, executorService);
            return merged.write(writer);
        } catch (IOException e) {
            throw MergingException.wrapException(e).build();
        } finally {
            logger.fine(String.format("write merge finished in %sms", timer.elapsed(TimeUnit.MILLISECONDS)));
        }
    }

    /**
     * Shutdown AOSP utilized thread-pool.
     */
    public void shutdown() {
        FullyQualifiedName.logCacheUsage(logger);
        // AOSP code never shuts down its singleton executor and leaves the process hanging.
        ExecutorSingleton.getExecutor().shutdownNow();
    }

    @Nullable
    private Path prepareOutputPath(@Nullable Path out) throws IOException {
        if (out == null) {
            return null;
        }
        return Files.createDirectories(out);
    }

    /** Deserializes a list of serialized resource paths to a {@link ParsedAndroidData}. */
    public ParsedAndroidData deserializeSymbolsToData(List<Path> symbolPaths) throws IOException, MergingException {
        AndroidDataSerializer serializer = AndroidDataSerializer.create();
        final ListeningExecutorService executorService = MoreExecutors
                .listeningDecorator(Executors.newFixedThreadPool(15));
        final Builder deserializedDataBuilder = ParsedAndroidData.Builder.newBuilder();
        try (Closeable closeable = ExecutorServiceCloser.createWith(executorService)) {
            List<ListenableFuture<Boolean>> deserializing = new ArrayList<>();
            for (final Path symbolPath : symbolPaths) {
                deserializing.add(
                        executorService.submit(new Deserialize(serializer, symbolPath, deserializedDataBuilder)));
            }
            FailedFutureAggregator<MergingException> aggregator = FailedFutureAggregator
                    .createForMergingExceptionWithMessage("Failure(s) during dependency parsing");
            aggregator.aggregateAndMaybeThrow(deserializing);
        }
        return deserializedDataBuilder.build();
    }

    /**
     * A FileVisitor that will add all files to be stored in a zip archive.
     */
    private static class ZipBuilderVisitor extends SimpleFileVisitor<Path> {

        // The earliest date representable in a zip file, 1-1-1980 (the DOS epoch).
        private static final long ZIP_EPOCH = 315561600000L;
        // ZIP timestamps have a resolution of 2 seconds.
        // see http://www.info-zip.org/FAQ.html#limits
        private static final long MINIMUM_TIMESTAMP_INCREMENT = 2000L;

        private final ZipOutputStream zip;
        protected final Path root;
        private final String directoryPrefix;
        private int storageMethod = ZipEntry.STORED;

        ZipBuilderVisitor(ZipOutputStream zip, Path root, String directory) {
            this.zip = zip;
            this.root = root;
            this.directoryPrefix = directory;
        }

        public void setCompress(boolean compress) {
            storageMethod = compress ? ZipEntry.DEFLATED : ZipEntry.STORED;
        }

        /**
         * Normalize timestamps for deterministic builds. Stamp .class files to be a bit newer
         * than .java files. See:
         * {@link com.google.devtools.build.buildjar.jarhelper.JarHelper#normalizedTimestamp(String)}
         */
        protected long normalizeTime(String filename) {
            if (filename.endsWith(".class")) {
                return ZIP_EPOCH + MINIMUM_TIMESTAMP_INCREMENT;
            } else {
                return ZIP_EPOCH;
            }
        }

        protected void addEntry(Path file, byte[] content) throws IOException {
            String prefix = directoryPrefix != null ? (directoryPrefix + "/") : "";
            String relativeName = root.relativize(file).toString();
            ZipEntry entry = new ZipEntry(prefix + relativeName);
            entry.setMethod(storageMethod);
            entry.setTime(normalizeTime(relativeName));
            entry.setSize(content.length);
            CRC32 crc32 = new CRC32();
            crc32.update(content);
            entry.setCrc(crc32.getValue());

            zip.putNextEntry(entry);
            zip.write(content);
            zip.closeEntry();
        }

        @Override
        public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
            byte[] content = Files.readAllBytes(file);
            addEntry(file, content);
            return FileVisitResult.CONTINUE;
        }
    }

    /**
     * A FileVisitor that will add all R.java files to be stored in a zip archive.
     */
    private static final class SymbolFileSrcJarBuildingVisitor extends ZipBuilderVisitor {

        static final Pattern PACKAGE_PATTERN = Pattern
                .compile("\\s*package ([a-zA-Z_$][a-zA-Z\\d_$]*(?:\\.[a-zA-Z_$][a-zA-Z\\d_$]*)*)");
        static final Pattern ID_PATTERN = Pattern.compile("public static int ([\\w\\.]+)=0x[0-9A-fa-f]+;");
        static final Pattern INNER_CLASS = Pattern.compile("public static class ([a-z_]*) \\{(.*?)\\}",
                Pattern.DOTALL);

        private final boolean staticIds;

        private SymbolFileSrcJarBuildingVisitor(ZipOutputStream zip, Path root, boolean staticIds) {
            super(zip, root, null);
            this.staticIds = staticIds;
        }

        private String replaceIdsWithStaticIds(String contents) {
            Matcher packageMatcher = PACKAGE_PATTERN.matcher(contents);
            if (!packageMatcher.find()) {
                return contents;
            }
            String pkg = packageMatcher.group(1);
            StringBuffer out = new StringBuffer();
            Matcher innerClassMatcher = INNER_CLASS.matcher(contents);
            while (innerClassMatcher.find()) {
                String resourceType = innerClassMatcher.group(1);
                Matcher idMatcher = ID_PATTERN.matcher(innerClassMatcher.group(2));
                StringBuffer resourceIds = new StringBuffer();
                while (idMatcher.find()) {
                    String javaId = idMatcher.group(1);
                    idMatcher.appendReplacement(resourceIds, String.format("public static int %s=0x%08X;", javaId,
                            Objects.hash(pkg, resourceType, javaId)));
                }
                idMatcher.appendTail(resourceIds);
                innerClassMatcher.appendReplacement(out,
                        String.format("public static class %s {%s}", resourceType, resourceIds.toString()));
            }
            innerClassMatcher.appendTail(out);
            return out.toString();
        }

        @Override
        public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
            if (file.getFileName().endsWith("R.java")) {
                byte[] content = Files.readAllBytes(file);
                if (staticIds) {
                    content = replaceIdsWithStaticIds(UTF_8.decode(ByteBuffer.wrap(content)).toString())
                            .getBytes(UTF_8);
                }
                addEntry(file, content);
            }
            return FileVisitResult.CONTINUE;
        }
    }

    /**
     * A FileVisitor that will add all R class files to be stored in a zip archive.
     */
    private static final class ClassJarBuildingVisitor extends ZipBuilderVisitor {

        ClassJarBuildingVisitor(ZipOutputStream zip, Path root) {
            super(zip, root, null);
        }

        @Override
        public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
            Path filename = file.getFileName();
            String name = filename.toString();
            if (name.endsWith(".class")) {
                byte[] content = Files.readAllBytes(file);
                addEntry(file, content);
            }
            return FileVisitResult.CONTINUE;
        }

        private byte[] manifestContent() throws IOException {
            Manifest manifest = new Manifest();
            Attributes attributes = manifest.getMainAttributes();
            attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0");
            Attributes.Name createdBy = new Attributes.Name("Created-By");
            if (attributes.getValue(createdBy) == null) {
                attributes.put(createdBy, "bazel");
            }
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            manifest.write(out);
            return out.toByteArray();
        }

        void writeManifestContent() throws IOException {
            addEntry(root.resolve(JarFile.MANIFEST_NAME), manifestContent());
        }
    }

    /** Task to deserialize resources from a path. */
    private static final class Deserialize implements Callable<Boolean> {

        private final Path symbolPath;

        private final Builder finalDataBuilder;
        private AndroidDataSerializer serializer;

        private Deserialize(AndroidDataSerializer serializer, Path symbolPath, Builder finalDataBuilder) {
            this.serializer = serializer;
            this.symbolPath = symbolPath;
            this.finalDataBuilder = finalDataBuilder;
        }

        @Override
        public Boolean call() throws Exception {
            final Builder parsedDataBuilder = ParsedAndroidData.Builder.newBuilder();
            serializer.read(symbolPath, parsedDataBuilder.consumers());
            // The builder isn't threadsafe, so synchronize the copyTo call.
            synchronized (finalDataBuilder) {
                // All the resources are sorted before writing, so they can be aggregated in
                // whatever order here.
                parsedDataBuilder.copyTo(finalDataBuilder);
            }
            return Boolean.TRUE;
        }
    }
}