Merge "AAPT2: Implement density stripping and initial Split support" into nyc-dev
diff --git a/tools/aapt2/Android.mk b/tools/aapt2/Android.mk
index cb82ac3..f9d35ab 100644
--- a/tools/aapt2/Android.mk
+++ b/tools/aapt2/Android.mk
@@ -47,6 +47,7 @@
 	proto/ProtoHelpers.cpp \
 	proto/TableProtoDeserializer.cpp \
 	proto/TableProtoSerializer.cpp \
+	split/TableSplitter.cpp \
 	unflatten/BinaryResourceParser.cpp \
 	unflatten/ResChunkPullParser.cpp \
 	util/BigBuffer.cpp \
@@ -90,6 +91,7 @@
 	link/XmlReferenceLinker_test.cpp \
 	process/SymbolTable_test.cpp \
 	proto/TableProtoSerializer_test.cpp \
+	split/TableSplitter_test.cpp \
 	util/BigBuffer_test.cpp \
 	util/Maybe_test.cpp \
 	util/StringPiece_test.cpp \
@@ -103,6 +105,7 @@
 	ResourceParser_test.cpp \
 	ResourceTable_test.cpp \
 	ResourceUtils_test.cpp \
+	SdkConstants_test.cpp \
 	StringPool_test.cpp \
 	ValueVisitor_test.cpp \
 	xml/XmlDom_test.cpp \
diff --git a/tools/aapt2/Debug.cpp b/tools/aapt2/Debug.cpp
index 4bea129..19bd521 100644
--- a/tools/aapt2/Debug.cpp
+++ b/tools/aapt2/Debug.cpp
@@ -30,7 +30,8 @@
 
 namespace aapt {
 
-struct PrintVisitor : public ValueVisitor {
+class PrintVisitor : public ValueVisitor {
+public:
     using ValueVisitor::visit;
 
     void visit(Attribute* attr) override {
@@ -69,7 +70,11 @@
         for (const auto& entry : style->entries) {
             std::cout << "\n        ";
             if (entry.key.name) {
-                std::cout << entry.key.name.value().package << ":" << entry.key.name.value().entry;
+                const ResourceName& name = entry.key.name.value();
+                if (!name.package.empty()) {
+                    std::cout << name.package << ":";
+                }
+                std::cout << name.entry;
             }
 
             if (entry.key.id) {
@@ -89,7 +94,21 @@
     }
 
     void visit(Styleable* styleable) override {
-        styleable->print(&std::cout);
+        std::cout << "(styleable)";
+        for (const auto& attr : styleable->entries) {
+            std::cout << "\n        ";
+            if (attr.name) {
+                const ResourceName& name = attr.name.value();
+                if (!name.package.empty()) {
+                    std::cout << name.package << ":";
+                }
+                std::cout << name.entry;
+            }
+
+            if (attr.id) {
+                std::cout << "(" << attr.id.value() << ")";
+            }
+        }
     }
 
     void visitItem(Item* item) override {
@@ -97,7 +116,9 @@
     }
 };
 
-void Debug::printTable(ResourceTable* table) {
+void Debug::printTable(ResourceTable* table, const DebugPrintTableOptions& options) {
+    PrintVisitor visitor;
+
     for (auto& package : table->packages) {
         std::cout << "Package name=" << package->name;
         if (package->id) {
@@ -106,7 +127,7 @@
         std::cout << std::endl;
 
         for (const auto& type : package->types) {
-            std::cout << "  type " << type->type;
+            std::cout << "\n  type " << type->type;
             if (type->id) {
                 std::cout << " id=" << std::hex << (int) type->id.value() << std::dec;
             }
@@ -142,10 +163,12 @@
 
                 std::cout << std::endl;
 
-                PrintVisitor visitor;
                 for (const auto& value : entry->values) {
                     std::cout << "      (" << value->config << ") ";
                     value->value->accept(&visitor);
+                    if (options.showSources && !value->value->getSource().path.empty()) {
+                        std::cout << " src=" << value->value->getSource();
+                    }
                     std::cout << std::endl;
                 }
             }
diff --git a/tools/aapt2/Debug.h b/tools/aapt2/Debug.h
index ba05be9..fbe6477 100644
--- a/tools/aapt2/Debug.h
+++ b/tools/aapt2/Debug.h
@@ -25,8 +25,12 @@
 
 namespace aapt {
 
+struct DebugPrintTableOptions {
+    bool showSources = false;
+};
+
 struct Debug {
-    static void printTable(ResourceTable* table);
+    static void printTable(ResourceTable* table, const DebugPrintTableOptions& options = {});
     static void printStyleGraph(ResourceTable* table,
                                 const ResourceName& targetStyle);
     static void dumpHex(const void* data, size_t len);
diff --git a/tools/aapt2/ResourceTable.cpp b/tools/aapt2/ResourceTable.cpp
index 3e73be4..8d734f3 100644
--- a/tools/aapt2/ResourceTable.cpp
+++ b/tools/aapt2/ResourceTable.cpp
@@ -277,20 +277,31 @@
                                      const Source& source,
                                      const StringPiece16& path,
                                      IDiagnostics* diag) {
-    return addFileReference(name, config, source, path, resolveValueCollision, diag);
+    return addFileReferenceImpl(name, config, source, path, nullptr, kValidNameChars, diag);
 }
 
-bool ResourceTable::addFileReference(const ResourceNameRef& name,
-                                     const ConfigDescription& config,
-                                     const Source& source,
-                                     const StringPiece16& path,
-                                     std::function<int(Value*,Value*)> conflictResolver,
-                                     IDiagnostics* diag) {
+bool ResourceTable::addFileReferenceAllowMangled(const ResourceNameRef& name,
+                                                 const ConfigDescription& config,
+                                                 const Source& source,
+                                                 const StringPiece16& path,
+                                                 io::IFile* file,
+                                                 IDiagnostics* diag) {
+    return addFileReferenceImpl(name, config, source, path, file, kValidNameMangledChars, diag);
+}
+
+bool ResourceTable::addFileReferenceImpl(const ResourceNameRef& name,
+                                         const ConfigDescription& config,
+                                         const Source& source,
+                                         const StringPiece16& path,
+                                         io::IFile* file,
+                                         const char16_t* validChars,
+                                         IDiagnostics* diag) {
     std::unique_ptr<FileReference> fileRef = util::make_unique<FileReference>(
             stringPool.makeRef(path));
     fileRef->setSource(source);
+    fileRef->file = file;
     return addResourceImpl(name, ResourceId{}, config, StringPiece{}, std::move(fileRef),
-                           kValidNameChars, conflictResolver, diag);
+                           kValidNameChars, resolveValueCollision, diag);
 }
 
 bool ResourceTable::addResourceAllowMangled(const ResourceNameRef& name,
diff --git a/tools/aapt2/ResourceTable.h b/tools/aapt2/ResourceTable.h
index 8ffff1f..7f5c2b8 100644
--- a/tools/aapt2/ResourceTable.h
+++ b/tools/aapt2/ResourceTable.h
@@ -23,6 +23,7 @@
 #include "ResourceValues.h"
 #include "Source.h"
 #include "StringPool.h"
+#include "io/File.h"
 
 #include <android-base/macros.h>
 #include <map>
@@ -202,17 +203,17 @@
                      IDiagnostics* diag);
 
     bool addFileReference(const ResourceNameRef& name,
-                          const ConfigDescription& config,
-                          const Source& source,
-                          const StringPiece16& path,
-                          IDiagnostics* diag);
+                              const ConfigDescription& config,
+                              const Source& source,
+                              const StringPiece16& path,
+                              IDiagnostics* diag);
 
-    bool addFileReference(const ResourceNameRef& name,
-                          const ConfigDescription& config,
-                          const Source& source,
-                          const StringPiece16& path,
-                          std::function<int(Value*,Value*)> conflictResolver,
-                          IDiagnostics* diag);
+    bool addFileReferenceAllowMangled(const ResourceNameRef& name,
+                                      const ConfigDescription& config,
+                                      const Source& source,
+                                      const StringPiece16& path,
+                                      io::IFile* file,
+                                      IDiagnostics* diag);
 
     /**
      * Same as addResource, but doesn't verify the validity of the name. This is used
@@ -280,6 +281,14 @@
 private:
     ResourceTablePackage* findOrCreatePackage(const StringPiece16& name);
 
+    bool addFileReferenceImpl(const ResourceNameRef& name,
+                              const ConfigDescription& config,
+                              const Source& source,
+                              const StringPiece16& path,
+                              io::IFile* file,
+                              const char16_t* validChars,
+                              IDiagnostics* diag);
+
     bool addResourceImpl(const ResourceNameRef& name,
                          ResourceId resId,
                          const ConfigDescription& config,
diff --git a/tools/aapt2/ResourceValues.cpp b/tools/aapt2/ResourceValues.cpp
index ab9c792..dd7ff01 100644
--- a/tools/aapt2/ResourceValues.cpp
+++ b/tools/aapt2/ResourceValues.cpp
@@ -18,6 +18,7 @@
 #include "ResourceUtils.h"
 #include "ResourceValues.h"
 #include "ValueVisitor.h"
+#include "io/File.h"
 #include "util/Util.h"
 
 #include <androidfw/ResourceTypes.h>
@@ -190,6 +191,7 @@
 
 FileReference* FileReference::clone(StringPool* newPool) const {
     FileReference* fr = new FileReference(newPool->makeRef(*path));
+    fr->file = file;
     fr->mComment = mComment;
     fr->mSource = mSource;
     return fr;
diff --git a/tools/aapt2/ResourceValues.h b/tools/aapt2/ResourceValues.h
index dc2e28e..43354ac 100644
--- a/tools/aapt2/ResourceValues.h
+++ b/tools/aapt2/ResourceValues.h
@@ -20,6 +20,7 @@
 #include "Diagnostics.h"
 #include "Resource.h"
 #include "StringPool.h"
+#include "io/File.h"
 #include "util/Maybe.h"
 
 #include <array>
@@ -226,6 +227,11 @@
 struct FileReference : public BaseItem<FileReference> {
     StringPool::Ref path;
 
+    /**
+     * A handle to the file object from which this file can be read.
+     */
+    io::IFile* file = nullptr;
+
     FileReference() = default;
     FileReference(const StringPool::Ref& path);
 
diff --git a/tools/aapt2/SdkConstants_test.cpp b/tools/aapt2/SdkConstants_test.cpp
new file mode 100644
index 0000000..e81f412
--- /dev/null
+++ b/tools/aapt2/SdkConstants_test.cpp
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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.
+ */
+
+#include "SdkConstants.h"
+
+#include <gtest/gtest.h>
+
+namespace aapt {
+
+TEST(SdkConstantsTest, FirstAttributeIsSdk1) {
+    EXPECT_EQ(1u, findAttributeSdkLevel(ResourceId(0x01010000)));
+}
+
+TEST(SdkConstantsTest, AllAttributesAfterLollipopAreLollipopMR1) {
+    EXPECT_EQ(SDK_LOLLIPOP, findAttributeSdkLevel(ResourceId(0x010103f7)));
+    EXPECT_EQ(SDK_LOLLIPOP, findAttributeSdkLevel(ResourceId(0x010104ce)));
+
+    EXPECT_EQ(SDK_LOLLIPOP_MR1, findAttributeSdkLevel(ResourceId(0x010104cf)));
+    EXPECT_EQ(SDK_LOLLIPOP_MR1, findAttributeSdkLevel(ResourceId(0x010104d8)));
+
+    EXPECT_EQ(SDK_LOLLIPOP_MR1, findAttributeSdkLevel(ResourceId(0x010104d9)));
+    EXPECT_EQ(SDK_LOLLIPOP_MR1, findAttributeSdkLevel(ResourceId(0x0101ffff)));
+}
+
+} // namespace aapt
diff --git a/tools/aapt2/compile/Compile.cpp b/tools/aapt2/compile/Compile.cpp
index 0dd8e18..5f9719e 100644
--- a/tools/aapt2/compile/Compile.cpp
+++ b/tools/aapt2/compile/Compile.cpp
@@ -424,8 +424,17 @@
 class CompileContext : public IAaptContext {
 private:
     StdErrDiagnostics mDiagnostics;
+    bool mVerbose = false;
 
 public:
+    void setVerbose(bool val) {
+        mVerbose = val;
+    }
+
+    bool verbose() override {
+        return mVerbose;
+    }
+
     IDiagnostics* getDiagnostics() override {
        return &mDiagnostics;
     }
@@ -453,8 +462,10 @@
  * Entry point for compilation phase. Parses arguments and dispatches to the correct steps.
  */
 int compile(const std::vector<StringPiece>& args) {
+    CompileContext context;
     CompileOptions options;
 
+    bool verbose = false;
     Flags flags = Flags()
             .requiredFlag("-o", "Output path", &options.outputPath)
             .optionalFlag("--dir", "Directory to scan for resources", &options.resDir)
@@ -462,12 +473,13 @@
                             "(en-XA and ar-XB)", &options.pseudolocalize)
             .optionalSwitch("--legacy", "Treat errors that used to be valid in AAPT as warnings",
                             &options.legacyMode)
-            .optionalSwitch("-v", "Enables verbose logging", &options.verbose);
+            .optionalSwitch("-v", "Enables verbose logging", &verbose);
     if (!flags.parse("aapt2 compile", args, &std::cerr)) {
         return 1;
     }
 
-    CompileContext context;
+    context.setVerbose(verbose);
+
     std::unique_ptr<IArchiveWriter> archiveWriter;
 
     std::vector<ResourcePathData> inputData;
diff --git a/tools/aapt2/dump/Dump.cpp b/tools/aapt2/dump/Dump.cpp
index 915fae8..ad7de0a 100644
--- a/tools/aapt2/dump/Dump.cpp
+++ b/tools/aapt2/dump/Dump.cpp
@@ -103,21 +103,32 @@
         return nullptr;
     }
 
+    bool verbose() override {
+        return mVerbose;
+    }
+
+    void setVerbose(bool val) {
+        mVerbose = val;
+    }
+
 private:
     StdErrDiagnostics mDiagnostics;
+    bool mVerbose = false;
 };
 
 /**
  * Entry point for dump command.
  */
 int dump(const std::vector<StringPiece>& args) {
-    //DumpOptions options;
-    Flags flags = Flags();
+    bool verbose = false;
+    Flags flags = Flags()
+            .optionalSwitch("-v", "increase verbosity of output", &verbose);
     if (!flags.parse("aapt2 dump", args, &std::cerr)) {
         return 1;
     }
 
     DumpContext context;
+    context.setVerbose(verbose);
 
     for (const std::string& arg : flags.getArgs()) {
         tryDumpFile(&context, arg);
diff --git a/tools/aapt2/link/Link.cpp b/tools/aapt2/link/Link.cpp
index 3437ac0..d83f6def 100644
--- a/tools/aapt2/link/Link.cpp
+++ b/tools/aapt2/link/Link.cpp
@@ -38,6 +38,7 @@
 #include "process/IResourceTableConsumer.h"
 #include "process/SymbolTable.h"
 #include "proto/ProtoSerialize.h"
+#include "split/TableSplitter.h"
 #include "unflatten/BinaryResourceParser.h"
 #include "util/Files.h"
 #include "util/StringPiece.h"
@@ -63,15 +64,14 @@
     bool noAutoVersion = false;
     bool staticLib = false;
     bool generateNonFinalIds = false;
-    bool verbose = false;
     bool outputToDirectory = false;
     bool autoAddOverlay = false;
     bool doNotCompressAnything = false;
     std::vector<std::string> extensionsToNotCompress;
     Maybe<std::u16string> privateSymbols;
     ManifestFixerOptions manifestFixerOptions;
-    IConfigFilter* configFilter = nullptr;
     std::unordered_set<std::string> products;
+    TableSplitterOptions tableSplitterOptions;
 };
 
 struct LinkContext : public IAaptContext {
@@ -80,6 +80,7 @@
     std::u16string mCompilationPackage;
     uint8_t mPackageId;
     std::unique_ptr<ISymbolTable> mSymbols;
+    bool mVerbose = false;
 
     IDiagnostics* getDiagnostics() override {
         return &mDiagnostics;
@@ -100,8 +101,376 @@
     ISymbolTable* getExternalSymbols() override {
         return mSymbols.get();
     }
+
+    bool verbose() override {
+        return mVerbose;
+    }
 };
 
+static bool copyFileToArchive(io::IFile* file, const std::string& outPath,
+                              uint32_t compressionFlags,
+                              IArchiveWriter* writer, IAaptContext* context) {
+    std::unique_ptr<io::IData> data = file->openAsData();
+    if (!data) {
+        context->getDiagnostics()->error(DiagMessage(file->getSource())
+                                         << "failed to open file");
+        return false;
+    }
+
+    CompiledFileInputStream inputStream(data->data(), data->size());
+    if (!inputStream.CompiledFile()) {
+        context->getDiagnostics()->error(DiagMessage(file->getSource())
+                                         << "invalid compiled file header");
+        return false;
+    }
+
+    if (context->verbose()) {
+        context->getDiagnostics()->note(DiagMessage() << "writing " << outPath << " to archive");
+    }
+
+    if (writer->startEntry(outPath, compressionFlags)) {
+        if (writer->writeEntry(reinterpret_cast<const uint8_t*>(inputStream.data()),
+                               inputStream.size())) {
+            if (writer->finishEntry()) {
+                return true;
+            }
+        }
+    }
+
+    context->getDiagnostics()->error(DiagMessage() << "failed to write file " << outPath);
+    return false;
+}
+
+static bool flattenXml(xml::XmlResource* xmlRes, const StringPiece& path, Maybe<size_t> maxSdkLevel,
+                       bool keepRawValues, IArchiveWriter* writer, IAaptContext* context) {
+    BigBuffer buffer(1024);
+    XmlFlattenerOptions options = {};
+    options.keepRawValues = keepRawValues;
+    options.maxSdkLevel = maxSdkLevel;
+    XmlFlattener flattener(&buffer, options);
+    if (!flattener.consume(context, xmlRes)) {
+        return false;
+    }
+
+    if (context->verbose()) {
+        DiagMessage msg;
+        msg << "writing " << path << " to archive";
+        if (maxSdkLevel) {
+            msg << " maxSdkLevel=" << maxSdkLevel.value();
+        }
+        context->getDiagnostics()->note(msg);
+    }
+
+    if (writer->startEntry(path, ArchiveEntry::kCompress)) {
+        if (writer->writeEntry(buffer)) {
+            if (writer->finishEntry()) {
+                return true;
+            }
+        }
+    }
+    context->getDiagnostics()->error(DiagMessage() << "failed to write " << path << " to archive");
+    return false;
+}
+
+/*static std::unique_ptr<ResourceTable> loadTable(const Source& source, const void* data, size_t len,
+                                                IDiagnostics* diag) {
+    std::unique_ptr<ResourceTable> table = util::make_unique<ResourceTable>();
+    BinaryResourceParser parser(diag, table.get(), source, data, len);
+    if (!parser.parse()) {
+        return {};
+    }
+    return table;
+}*/
+
+static std::unique_ptr<ResourceTable> loadTableFromPb(const Source& source,
+                                                      const void* data, size_t len,
+                                                      IDiagnostics* diag) {
+    pb::ResourceTable pbTable;
+    if (!pbTable.ParseFromArray(data, len)) {
+        diag->error(DiagMessage(source) << "invalid compiled table");
+        return {};
+    }
+
+    std::unique_ptr<ResourceTable> table = deserializeTableFromPb(pbTable, source, diag);
+    if (!table) {
+        return {};
+    }
+    return table;
+}
+
+/**
+ * Inflates an XML file from the source path.
+ */
+static std::unique_ptr<xml::XmlResource> loadXml(const std::string& path, IDiagnostics* diag) {
+    std::ifstream fin(path, std::ifstream::binary);
+    if (!fin) {
+        diag->error(DiagMessage(path) << strerror(errno));
+        return {};
+    }
+    return xml::inflate(&fin, diag, Source(path));
+}
+
+static std::unique_ptr<xml::XmlResource> loadBinaryXmlSkipFileExport(const Source& source,
+                                                                     const void* data, size_t len,
+                                                                     IDiagnostics* diag) {
+    CompiledFileInputStream inputStream(data, len);
+    if (!inputStream.CompiledFile()) {
+        diag->error(DiagMessage(source) << "invalid compiled file header");
+        return {};
+    }
+
+    const uint8_t* xmlData = reinterpret_cast<const uint8_t*>(inputStream.data());
+    const size_t xmlDataLen = inputStream.size();
+
+    std::unique_ptr<xml::XmlResource> xmlRes = xml::inflate(xmlData, xmlDataLen, diag, source);
+    if (!xmlRes) {
+        return {};
+    }
+    return xmlRes;
+}
+
+static std::unique_ptr<ResourceFile> loadFileExportHeader(const Source& source,
+                                                          const void* data, size_t len,
+                                                          IDiagnostics* diag) {
+    CompiledFileInputStream inputStream(data, len);
+    const pb::CompiledFile* pbFile = inputStream.CompiledFile();
+    if (!pbFile) {
+        diag->error(DiagMessage(source) << "invalid compiled file header");
+        return {};
+    }
+
+    std::unique_ptr<ResourceFile> resFile = deserializeCompiledFileFromPb(*pbFile, source, diag);
+    if (!resFile) {
+        return {};
+    }
+    return resFile;
+}
+
+struct ResourceFileFlattenerOptions {
+    bool noAutoVersion = false;
+    bool keepRawValues = false;
+    bool doNotCompressAnything = false;
+    std::vector<std::string> extensionsToNotCompress;
+};
+
+class ResourceFileFlattener {
+public:
+    ResourceFileFlattener(const ResourceFileFlattenerOptions& options,
+                          IAaptContext* context, proguard::KeepSet* keepSet) :
+            mOptions(options), mContext(context), mKeepSet(keepSet) {
+    }
+
+    bool flatten(ResourceTable* table, IArchiveWriter* archiveWriter);
+
+private:
+    struct FileOperation {
+        io::IFile* fileToCopy;
+        std::unique_ptr<xml::XmlResource> xmlToFlatten;
+        std::string dstPath;
+    };
+
+    uint32_t getCompressionFlags(const StringPiece& str);
+
+    std::unique_ptr<xml::XmlResource> linkAndVersionXmlFile(const ResourceEntry* entry,
+                                                            const ResourceFile& fileDesc,
+                                                            io::IFile* file,
+                                                            ResourceTable* table);
+
+    ResourceFileFlattenerOptions mOptions;
+    IAaptContext* mContext;
+    proguard::KeepSet* mKeepSet;
+};
+
+uint32_t ResourceFileFlattener::getCompressionFlags(const StringPiece& str) {
+    if (mOptions.doNotCompressAnything) {
+        return 0;
+    }
+
+    for (const std::string& extension : mOptions.extensionsToNotCompress) {
+        if (util::stringEndsWith<char>(str, extension)) {
+            return 0;
+        }
+    }
+    return ArchiveEntry::kCompress;
+}
+
+std::unique_ptr<xml::XmlResource> ResourceFileFlattener::linkAndVersionXmlFile(
+        const ResourceEntry* entry,
+        const ResourceFile& fileDesc,
+        io::IFile* file,
+        ResourceTable* table) {
+    const StringPiece srcPath = file->getSource().path;
+    if (mContext->verbose()) {
+        mContext->getDiagnostics()->note(DiagMessage() << "linking " << srcPath);
+    }
+
+    std::unique_ptr<io::IData> data = file->openAsData();
+    if (!data) {
+        mContext->getDiagnostics()->error(DiagMessage(file->getSource()) << "failed to open file");
+        return {};
+    }
+
+    std::unique_ptr<xml::XmlResource> xmlRes;
+    if (util::stringEndsWith<char>(srcPath, ".flat")) {
+        xmlRes = loadBinaryXmlSkipFileExport(file->getSource(), data->data(), data->size(),
+                                             mContext->getDiagnostics());
+    } else {
+        xmlRes = xml::inflate(data->data(), data->size(), mContext->getDiagnostics(),
+                              file->getSource());
+    }
+
+    if (!xmlRes) {
+        return {};
+    }
+
+    // Copy the the file description header.
+    xmlRes->file = fileDesc;
+
+    XmlReferenceLinker xmlLinker;
+    if (!xmlLinker.consume(mContext, xmlRes.get())) {
+        return {};
+    }
+
+    if (!proguard::collectProguardRules(xmlRes->file.source, xmlRes.get(), mKeepSet)) {
+        return {};
+    }
+
+    if (!mOptions.noAutoVersion) {
+        // Find the first SDK level used that is higher than this defined config and
+        // not superseded by a lower or equal SDK level resource.
+        for (int sdkLevel : xmlLinker.getSdkLevels()) {
+            if (sdkLevel > xmlRes->file.config.sdkVersion) {
+                if (!shouldGenerateVersionedResource(entry, xmlRes->file.config, sdkLevel)) {
+                    // If we shouldn't generate a versioned resource, stop checking.
+                    break;
+                }
+
+                ResourceFile versionedFileDesc = xmlRes->file;
+                versionedFileDesc.config.sdkVersion = sdkLevel;
+
+                if (mContext->verbose()) {
+                    mContext->getDiagnostics()->note(DiagMessage(versionedFileDesc.source)
+                                                     << "auto-versioning resource from config '"
+                                                     << xmlRes->file.config << "' -> '"
+                                                     << versionedFileDesc.config << "'");
+                }
+
+                std::u16string genPath = util::utf8ToUtf16(ResourceUtils::buildResourceFileName(
+                        versionedFileDesc, mContext->getNameMangler()));
+
+                bool added = table->addFileReferenceAllowMangled(versionedFileDesc.name,
+                                                                 versionedFileDesc.config,
+                                                                 versionedFileDesc.source,
+                                                                 genPath,
+                                                                 file,
+                                                                 mContext->getDiagnostics());
+                if (!added) {
+                    return {};
+                }
+                break;
+            }
+        }
+    }
+    return xmlRes;
+}
+
+/**
+ * Do not insert or remove any resources while executing in this function. It will
+ * corrupt the iteration order.
+ */
+bool ResourceFileFlattener::flatten(ResourceTable* table, IArchiveWriter* archiveWriter) {
+    bool error = false;
+    std::map<std::pair<ConfigDescription, StringPiece16>, FileOperation> configSortedFiles;
+
+    for (auto& pkg : table->packages) {
+        for (auto& type : pkg->types) {
+            // Sort by config and name, so that we get better locality in the zip file.
+            configSortedFiles.clear();
+            for (auto& entry : type->entries) {
+                // Iterate via indices because auto generated values can be inserted ahead of
+                // the value being processed.
+                for (size_t i = 0; i < entry->values.size(); i++) {
+                    ResourceConfigValue* configValue = entry->values[i].get();
+
+                    FileReference* fileRef = valueCast<FileReference>(configValue->value.get());
+                    if (!fileRef) {
+                        continue;
+                    }
+
+                    io::IFile* file = fileRef->file;
+                    if (!file) {
+                        mContext->getDiagnostics()->error(DiagMessage(fileRef->getSource())
+                                                          << "file not found");
+                        return false;
+                    }
+
+                    FileOperation fileOp;
+                    fileOp.dstPath = util::utf16ToUtf8(*fileRef->path);
+
+                    const StringPiece srcPath = file->getSource().path;
+                    if (type->type != ResourceType::kRaw &&
+                            (util::stringEndsWith<char>(srcPath, ".xml.flat") ||
+                            util::stringEndsWith<char>(srcPath, ".xml"))) {
+                        ResourceFile fileDesc;
+                        fileDesc.config = configValue->config;
+                        fileDesc.name = ResourceName(pkg->name, type->type, entry->name);
+                        fileDesc.source = fileRef->getSource();
+                        fileOp.xmlToFlatten = linkAndVersionXmlFile(entry.get(), fileDesc,
+                                                                    file, table);
+                        if (!fileOp.xmlToFlatten) {
+                            error = true;
+                            continue;
+                        }
+
+                    } else {
+                        fileOp.fileToCopy = file;
+                    }
+
+                    // NOTE(adamlesinski): Explicitly construct a StringPiece16 here, or else
+                    // we end up copying the string in the std::make_pair() method, then creating
+                    // a StringPiece16 from the copy, which would cause us to end up referencing
+                    // garbage in the map.
+                    const StringPiece16 entryName(entry->name);
+                    configSortedFiles[std::make_pair(configValue->config, entryName)] =
+                                      std::move(fileOp);
+                }
+            }
+
+            if (error) {
+                return false;
+            }
+
+            // Now flatten the sorted values.
+            for (auto& mapEntry : configSortedFiles) {
+                const ConfigDescription& config = mapEntry.first.first;
+                const FileOperation& fileOp = mapEntry.second;
+
+                if (fileOp.xmlToFlatten) {
+                    Maybe<size_t> maxSdkLevel;
+                    if (!mOptions.noAutoVersion) {
+                        maxSdkLevel = std::max<size_t>(config.sdkVersion, 1u);
+                    }
+
+                    bool result = flattenXml(fileOp.xmlToFlatten.get(), fileOp.dstPath, maxSdkLevel,
+                                             mOptions.keepRawValues,
+                                             archiveWriter, mContext);
+                    if (!result) {
+                        error = true;
+                    }
+                } else {
+                    bool result = copyFileToArchive(fileOp.fileToCopy, fileOp.dstPath,
+                                                    getCompressionFlags(fileOp.dstPath),
+                                                    archiveWriter, mContext);
+                    if (!result) {
+                        error = true;
+                    }
+                }
+            }
+        }
+    }
+    return !error;
+}
+
 class LinkCommand {
 public:
     LinkCommand(LinkContext* context, const LinkOptions& options) :
@@ -123,7 +492,7 @@
     std::unique_ptr<ISymbolTable> createSymbolTableFromIncludePaths() {
         AssetManagerSymbolTableBuilder builder;
         for (const std::string& path : mOptions.includePaths) {
-            if (mOptions.verbose) {
+            if (mContext->verbose()) {
                 mContext->getDiagnostics()->note(DiagMessage(path) << "loading include path");
             }
 
@@ -140,125 +509,6 @@
         return builder.build();
     }
 
-    std::unique_ptr<ResourceTable> loadTable(const Source& source, const void* data, size_t len) {
-        std::unique_ptr<ResourceTable> table = util::make_unique<ResourceTable>();
-        BinaryResourceParser parser(mContext, table.get(), source, data, len);
-        if (!parser.parse()) {
-            return {};
-        }
-        return table;
-    }
-
-    std::unique_ptr<ResourceTable> loadTableFromPb(const Source& source,
-                                                   const void* data, size_t len) {
-        pb::ResourceTable pbTable;
-        if (!pbTable.ParseFromArray(data, len)) {
-            mContext->getDiagnostics()->error(DiagMessage(source) << "invalid compiled table");
-            return {};
-        }
-
-        std::unique_ptr<ResourceTable> table = deserializeTableFromPb(pbTable, source,
-                                                                      mContext->getDiagnostics());
-        if (!table) {
-            return {};
-        }
-        return table;
-    }
-
-    /**
-     * Inflates an XML file from the source path.
-     */
-    static std::unique_ptr<xml::XmlResource> loadXml(const std::string& path, IDiagnostics* diag) {
-        std::ifstream fin(path, std::ifstream::binary);
-        if (!fin) {
-            diag->error(DiagMessage(path) << strerror(errno));
-            return {};
-        }
-
-        return xml::inflate(&fin, diag, Source(path));
-    }
-
-    static std::unique_ptr<xml::XmlResource> loadBinaryXmlSkipFileExport(
-            const Source& source,
-            const void* data, size_t len,
-            IDiagnostics* diag) {
-        CompiledFileInputStream inputStream(data, len);
-        if (!inputStream.CompiledFile()) {
-            diag->error(DiagMessage(source) << "invalid compiled file header");
-            return {};
-        }
-
-        const uint8_t* xmlData = reinterpret_cast<const uint8_t*>(inputStream.data());
-        const size_t xmlDataLen = inputStream.size();
-
-        std::unique_ptr<xml::XmlResource> xmlRes = xml::inflate(xmlData, xmlDataLen, diag, source);
-        if (!xmlRes) {
-            return {};
-        }
-        return xmlRes;
-    }
-
-    static std::unique_ptr<ResourceFile> loadFileExportHeader(const Source& source,
-                                                              const void* data, size_t len,
-                                                              IDiagnostics* diag) {
-        CompiledFileInputStream inputStream(data, len);
-        const pb::CompiledFile* pbFile = inputStream.CompiledFile();
-        if (!pbFile) {
-            diag->error(DiagMessage(source) << "invalid compiled file header");
-            return {};
-        }
-
-        std::unique_ptr<ResourceFile> resFile = deserializeCompiledFileFromPb(*pbFile, source,
-                                                                              diag);
-        if (!resFile) {
-            return {};
-        }
-        return resFile;
-    }
-
-    uint32_t getCompressionFlags(const StringPiece& str) {
-        if (mOptions.doNotCompressAnything) {
-            return 0;
-        }
-
-        for (const std::string& extension : mOptions.extensionsToNotCompress) {
-            if (util::stringEndsWith<char>(str, extension)) {
-                return 0;
-            }
-        }
-        return ArchiveEntry::kCompress;
-    }
-
-    bool copyFileToArchive(io::IFile* file, const std::string& outPath,
-                           IArchiveWriter* writer) {
-        std::unique_ptr<io::IData> data = file->openAsData();
-        if (!data) {
-            mContext->getDiagnostics()->error(DiagMessage(file->getSource())
-                                             << "failed to open file");
-            return false;
-        }
-
-        CompiledFileInputStream inputStream(data->data(), data->size());
-        if (!inputStream.CompiledFile()) {
-            mContext->getDiagnostics()->error(DiagMessage(file->getSource())
-                                              << "invalid compiled file header");
-            return false;
-        }
-
-        if (writer->startEntry(outPath, getCompressionFlags(outPath))) {
-            if (writer->writeEntry(reinterpret_cast<const uint8_t*>(inputStream.data()),
-                                   inputStream.size())) {
-                if (writer->finishEntry()) {
-                    return true;
-                }
-            }
-        }
-
-        mContext->getDiagnostics()->error(
-                DiagMessage(mOptions.outputPath) << "failed to write file " << outPath);
-        return false;
-    }
-
     Maybe<AppInfo> extractAppInfoFromManifest(xml::XmlResource* xmlRes) {
         // Make sure the first element is <manifest> with package attribute.
         if (xml::Element* manifestEl = xml::findRootElement(xmlRes->root.get())) {
@@ -349,28 +599,6 @@
         return false;
     }
 
-    bool flattenXml(xml::XmlResource* xmlRes, const StringPiece& path, Maybe<size_t> maxSdkLevel,
-                    IArchiveWriter* writer) {
-        BigBuffer buffer(1024);
-        XmlFlattenerOptions options = {};
-        options.keepRawValues = mOptions.staticLib;
-        options.maxSdkLevel = maxSdkLevel;
-        XmlFlattener flattener(&buffer, options);
-        if (!flattener.consume(mContext, xmlRes)) {
-            return false;
-        }
-
-        if (writer->startEntry(path, ArchiveEntry::kCompress)) {
-            if (writer->writeEntry(buffer)) {
-                if (writer->finishEntry()) {
-                    return true;
-                }
-            }
-        }
-        mContext->getDiagnostics()->error(
-                DiagMessage() << "failed to write " << path << " to archive");
-        return false;
-    }
 
     bool writeJavaFile(ResourceTable* table, const StringPiece16& packageNameToGenerate,
                        const StringPiece16& outPackage, JavaClassGeneratorOptions javaOptions) {
@@ -456,7 +684,7 @@
     }
 
     bool mergeResourceTable(io::IFile* file, bool override) {
-        if (mOptions.verbose) {
+        if (mContext->verbose()) {
             mContext->getDiagnostics()->note(DiagMessage() << "linking " << file->getSource());
         }
 
@@ -467,8 +695,9 @@
             return false;
         }
 
-        std::unique_ptr<ResourceTable> table = loadTableFromPb(file->getSource(), data->data(),
-                                                               data->size());
+        std::unique_ptr<ResourceTable> table = loadTableFromPb(file->getSource(),
+                                                               data->data(), data->size(),
+                                                               mContext->getDiagnostics());
         if (!table) {
             return false;
         }
@@ -483,7 +712,7 @@
     }
 
     bool mergeCompiledFile(io::IFile* file, std::unique_ptr<ResourceFile> fileDesc, bool overlay) {
-        if (mOptions.verbose) {
+        if (mContext->verbose()) {
             mContext->getDiagnostics()->note(DiagMessage() << "adding " << file->getSource());
         }
 
@@ -628,10 +857,9 @@
 
         TableMergerOptions tableMergerOptions;
         tableMergerOptions.autoAddOverlay = mOptions.autoAddOverlay;
-        tableMergerOptions.filter = mOptions.configFilter;
         mTableMerger = util::make_unique<TableMerger>(mContext, &mFinalTable, tableMergerOptions);
 
-        if (mOptions.verbose) {
+        if (mContext->verbose()) {
             mContext->getDiagnostics()->note(
                     DiagMessage() << "linking package '" << mContext->mCompilationPackage << "' "
                                   << "with package ID " << std::hex << (int) mContext->mPackageId);
@@ -692,6 +920,15 @@
                 mContext->getDiagnostics()->error(DiagMessage() << "failed stripping products");
                 return 1;
             }
+
+            // TODO(adamlesinski): Actually pass in split constraints and handle splits at the file
+            // level.
+            TableSplitter tableSplitter({}, mOptions.tableSplitterOptions);
+            if (!tableSplitter.verifySplitConstraints(mContext)) {
+                return 1;
+            }
+
+            tableSplitter.splitTable(&mFinalTable);
         }
 
         proguard::KeepSet proguardKeepSet;
@@ -728,8 +965,10 @@
                     }
                 }
 
-                if (!flattenXml(manifestXml.get(), "AndroidManifest.xml", {},
-                                archiveWriter.get())) {
+                const bool keepRawValues = mOptions.staticLib;
+                bool result = flattenXml(manifestXml.get(), "AndroidManifest.xml", {},
+                                         keepRawValues, archiveWriter.get(), mContext);
+                if (!result) {
                     error = true;
                 }
             } else {
@@ -742,113 +981,14 @@
             return 1;
         }
 
-        for (auto& mergeEntry : mTableMerger->getFilesToMerge()) {
-            const ResourceKeyRef& key = mergeEntry.first;
-            const FileToMerge& fileToMerge = mergeEntry.second;
+        ResourceFileFlattenerOptions fileFlattenerOptions;
+        fileFlattenerOptions.keepRawValues = mOptions.staticLib;
+        fileFlattenerOptions.doNotCompressAnything = mOptions.doNotCompressAnything;
+        fileFlattenerOptions.extensionsToNotCompress = mOptions.extensionsToNotCompress;
+        fileFlattenerOptions.noAutoVersion = mOptions.noAutoVersion;
+        ResourceFileFlattener fileFlattener(fileFlattenerOptions, mContext, &proguardKeepSet);
 
-            const StringPiece path = fileToMerge.file->getSource().path;
-
-            if (key.name.type != ResourceType::kRaw &&
-                    (util::stringEndsWith<char>(path, ".xml.flat") ||
-                    util::stringEndsWith<char>(path, ".xml"))) {
-                if (mOptions.verbose) {
-                    mContext->getDiagnostics()->note(DiagMessage() << "linking " << path);
-                }
-
-                io::IFile* file = fileToMerge.file;
-                std::unique_ptr<io::IData> data = file->openAsData();
-                if (!data) {
-                    mContext->getDiagnostics()->error(DiagMessage(file->getSource())
-                                                     << "failed to open file");
-                    return 1;
-                }
-
-                std::unique_ptr<xml::XmlResource> xmlRes;
-                if (util::stringEndsWith<char>(path, ".flat")) {
-                    xmlRes = loadBinaryXmlSkipFileExport(file->getSource(),
-                                                         data->data(), data->size(),
-                                                         mContext->getDiagnostics());
-                } else {
-                    xmlRes = xml::inflate(data->data(), data->size(), mContext->getDiagnostics(),
-                                          file->getSource());
-                }
-
-                if (!xmlRes) {
-                    return 1;
-                }
-
-                // Create the file description header.
-                xmlRes->file = ResourceFile{
-                        key.name.toResourceName(),
-                        key.config,
-                        fileToMerge.originalSource,
-                };
-
-                XmlReferenceLinker xmlLinker;
-                if (xmlLinker.consume(mContext, xmlRes.get())) {
-                    if (!proguard::collectProguardRules(xmlRes->file.source, xmlRes.get(),
-                                                        &proguardKeepSet)) {
-                        error = true;
-                    }
-
-                    Maybe<size_t> maxSdkLevel;
-                    if (!mOptions.noAutoVersion) {
-                        maxSdkLevel = std::max<size_t>(xmlRes->file.config.sdkVersion, 1u);
-                    }
-
-                    if (!flattenXml(xmlRes.get(), fileToMerge.dstPath, maxSdkLevel,
-                                    archiveWriter.get())) {
-                        error = true;
-                    }
-
-                    if (!mOptions.noAutoVersion) {
-                        Maybe<ResourceTable::SearchResult> result = mFinalTable.findResource(
-                                xmlRes->file.name);
-                        for (int sdkLevel : xmlLinker.getSdkLevels()) {
-                            if (sdkLevel > xmlRes->file.config.sdkVersion &&
-                                    shouldGenerateVersionedResource(result.value().entry,
-                                                                    xmlRes->file.config,
-                                                                    sdkLevel)) {
-                                xmlRes->file.config.sdkVersion = sdkLevel;
-
-                                std::string genResourcePath = ResourceUtils::buildResourceFileName(
-                                        xmlRes->file, mContext->getNameMangler());
-
-                                bool added = mFinalTable.addFileReference(
-                                        xmlRes->file.name,
-                                        xmlRes->file.config,
-                                        xmlRes->file.source,
-                                        util::utf8ToUtf16(genResourcePath),
-                                        mContext->getDiagnostics());
-                                if (!added) {
-                                    error = true;
-                                    continue;
-                                }
-
-                                if (!flattenXml(xmlRes.get(), genResourcePath, sdkLevel,
-                                                archiveWriter.get())) {
-                                    error = true;
-                                }
-                            }
-                        }
-                    }
-
-                } else {
-                    error = true;
-                }
-            } else {
-                if (mOptions.verbose) {
-                    mContext->getDiagnostics()->note(DiagMessage() << "copying " << path);
-                }
-
-                if (!copyFileToArchive(fileToMerge.file, fileToMerge.dstPath,
-                                       archiveWriter.get())) {
-                    error = true;
-                }
-            }
-        }
-
-        if (error) {
+        if (!fileFlattener.flatten(&mFinalTable, archiveWriter.get())) {
             mContext->getDiagnostics()->error(DiagMessage() << "failed linking file resources");
             return 1;
         }
@@ -912,8 +1052,10 @@
             }
         }
 
-        if (mOptions.verbose) {
-            Debug::printTable(&mFinalTable);
+        if (mContext->verbose()) {
+            DebugPrintTableOptions debugPrintTableOptions;
+            debugPrintTableOptions.showSources = true;
+            Debug::printTable(&mFinalTable, debugPrintTableOptions);
         }
         return 0;
     }
@@ -934,6 +1076,7 @@
 };
 
 int link(const std::vector<StringPiece>& args) {
+    LinkContext context;
     LinkOptions options;
     Maybe<std::string> privateSymbolsPackage;
     Maybe<std::string> minSdkVersion, targetSdkVersion;
@@ -942,6 +1085,7 @@
     Maybe<std::string> customJavaPackage;
     std::vector<std::string> extraJavaPackages;
     Maybe<std::string> configs;
+    Maybe<std::string> preferredDensity;
     Maybe<std::string> productList;
     bool legacyXFlag = false;
     bool requireLocalization = false;
@@ -966,6 +1110,9 @@
                             &requireLocalization)
             .optionalFlag("-c", "Comma separated list of configurations to include. The default\n"
                                 "is all configurations", &configs)
+            .optionalFlag("--preferred-density",
+                          "Selects the closest matching density and strips out all others.",
+                          &preferredDensity)
             .optionalFlag("--product", "Comma separated list of product names to keep",
                           &productList)
             .optionalSwitch("--output-to-dir", "Outputs the APK contents to a directory specified "
@@ -1001,14 +1148,12 @@
                           &renameInstrumentationTargetPackage)
             .optionalFlagList("-0", "File extensions not to compress",
                               &options.extensionsToNotCompress)
-            .optionalSwitch("-v", "Enables verbose logging", &options.verbose);
+            .optionalSwitch("-v", "Enables verbose logging", &context.mVerbose);
 
     if (!flags.parse("aapt2 link", args, &std::cerr)) {
         return 1;
     }
 
-    LinkContext context;
-
     if (privateSymbolsPackage) {
         options.privateSymbols = util::utf8ToUtf16(privateSymbolsPackage.value());
     }
@@ -1082,7 +1227,29 @@
             }
         }
 
-        options.configFilter = &filter;
+        options.tableSplitterOptions.configFilter = &filter;
+    }
+
+    if (preferredDensity) {
+        ConfigDescription preferredDensityConfig;
+        if (!ConfigDescription::parse(preferredDensity.value(), &preferredDensityConfig)) {
+            context.getDiagnostics()->error(DiagMessage() << "invalid density '"
+                                            << preferredDensity.value()
+                                            << "' for --preferred-density option");
+            return 1;
+        }
+
+        // Clear the version that can be automatically added.
+        preferredDensityConfig.sdkVersion = 0;
+
+        if (preferredDensityConfig.diff(ConfigDescription::defaultConfig())
+                != ConfigDescription::CONFIG_DENSITY) {
+            context.getDiagnostics()->error(DiagMessage() << "invalid preferred density '"
+                                            << preferredDensity.value() << "'. "
+                                            << "Preferred density must only be a density value");
+            return 1;
+        }
+        options.tableSplitterOptions.preferredDensity = preferredDensityConfig.density;
     }
 
     LinkCommand cmd(&context, options);
diff --git a/tools/aapt2/link/TableMerger.cpp b/tools/aapt2/link/TableMerger.cpp
index 2ecd5b0..5f11745 100644
--- a/tools/aapt2/link/TableMerger.cpp
+++ b/tools/aapt2/link/TableMerger.cpp
@@ -98,8 +98,7 @@
                 return false;
             }
 
-            mFilesToMerge[ResourceKeyRef(name, config)] = FileToMerge{
-                    f, oldFile->getSource(), util::utf16ToUtf8(*newFile->path) };
+            newFile->file = f;
             return true;
         };
 
@@ -198,10 +197,6 @@
             for (auto& srcValue : srcEntry->values) {
                 ResourceConfigValue* dstValue = dstEntry->findValue(srcValue->config,
                                                                     srcValue->product);
-
-                const bool stripConfig = mOptions.filter ?
-                        !mOptions.filter->match(srcValue->config) : false;
-
                 if (dstValue) {
                     const int collisionResult = ResourceTable::resolveValueCollision(
                             dstValue->value.get(), srcValue->value.get());
@@ -227,10 +222,6 @@
 
                 }
 
-                if (stripConfig) {
-                    continue;
-                }
-
                 if (!dstValue) {
                     // Force create the entry if we didn't have it.
                     dstValue = dstEntry->findOrCreateValue(srcValue->config, srcValue->product);
@@ -286,6 +277,7 @@
     std::unique_ptr<FileReference> fileRef = util::make_unique<FileReference>(
             table.stringPool.makeRef(path));
     fileRef->setSource(fileDesc.source);
+    fileRef->file = file;
 
     ResourceTablePackage* pkg = table.createPackage(fileDesc.name.package, 0x0);
     pkg->findOrCreateType(fileDesc.name.type)
@@ -293,15 +285,8 @@
             ->findOrCreateValue(fileDesc.config, {})
             ->value = std::move(fileRef);
 
-    auto callback = [&](const ResourceNameRef& name, const ConfigDescription& config,
-                       FileReference* newFile, FileReference* oldFile) -> bool {
-        mFilesToMerge[ResourceKeyRef(name, config)] = FileToMerge{
-                file, oldFile->getSource(), util::utf16ToUtf8(*newFile->path) };
-        return true;
-    };
-
     return doMerge(file->getSource(), &table, pkg,
-                   false /* mangle */, overlay /* overlay */, true /* allow new */, callback);
+                   false /* mangle */, overlay /* overlay */, true /* allow new */, {});
 }
 
 bool TableMerger::mergeFile(const ResourceFile& fileDesc, io::IFile* file) {
diff --git a/tools/aapt2/link/TableMerger.h b/tools/aapt2/link/TableMerger.h
index 4539679..b3c22dd 100644
--- a/tools/aapt2/link/TableMerger.h
+++ b/tools/aapt2/link/TableMerger.h
@@ -30,33 +30,11 @@
 
 namespace aapt {
 
-struct FileToMerge {
-    /**
-     * The compiled file from which to read the data.
-     */
-    io::IFile* file;
-
-    /**
-     * Where the original, uncompiled file came from.
-     */
-    Source originalSource;
-
-    /**
-     * The destination path within the APK/archive.
-     */
-    std::string dstPath;
-};
-
 struct TableMergerOptions {
     /**
      * If true, resources in overlays can be added without previously having existed.
      */
     bool autoAddOverlay = false;
-
-    /**
-     * A filter that removes resources whose configurations don't match.
-     */
-    IConfigFilter* filter = nullptr;
 };
 
 /**
@@ -81,10 +59,6 @@
      */
     TableMerger(IAaptContext* context, ResourceTable* outTable, const TableMergerOptions& options);
 
-    const std::map<ResourceKeyRef, FileToMerge>& getFilesToMerge() {
-        return mFilesToMerge;
-    }
-
     const std::set<std::u16string>& getMergedPackages() const {
         return mMergedPackages;
     }
@@ -119,8 +93,7 @@
 private:
     using FileMergeCallback = std::function<bool(const ResourceNameRef&,
                                                  const ConfigDescription& config,
-                                                 FileReference*,
-                                                 FileReference*)>;
+                                                 FileReference*, FileReference*)>;
 
     IAaptContext* mContext;
     ResourceTable* mMasterTable;
@@ -128,7 +101,6 @@
     ResourceTablePackage* mMasterPackage;
 
     std::set<std::u16string> mMergedPackages;
-    std::map<ResourceKeyRef, FileToMerge> mFilesToMerge;
 
     bool mergeFileImpl(const ResourceFile& fileDesc, io::IFile* file, bool overlay);
 
diff --git a/tools/aapt2/link/TableMerger_test.cpp b/tools/aapt2/link/TableMerger_test.cpp
index 45c8c98..4a80d3f 100644
--- a/tools/aapt2/link/TableMerger_test.cpp
+++ b/tools/aapt2/link/TableMerger_test.cpp
@@ -97,16 +97,6 @@
                                                                  test::parseConfigOrDie("hdpi-v4"));
     ASSERT_NE(nullptr, file);
     EXPECT_EQ(std::u16string(u"res/layout-hdpi-v4/main.xml"), *file->path);
-
-    ResourceName name = test::parseNameOrDie(u"@com.app.a:layout/main");
-    ResourceKeyRef key = { name, test::parseConfigOrDie("hdpi-v4") };
-
-    auto iter = merger.getFilesToMerge().find(key);
-    ASSERT_NE(merger.getFilesToMerge().end(), iter);
-
-    const FileToMerge& actualFileToMerge = iter->second;
-    EXPECT_EQ(&testFile, actualFileToMerge.file);
-    EXPECT_EQ(std::string("res/layout-hdpi-v4/main.xml"), actualFileToMerge.dstPath);
 }
 
 TEST_F(TableMergerTest, MergeFileOverlay) {
@@ -122,14 +112,6 @@
 
     ASSERT_TRUE(merger.mergeFile(fileDesc, &fileA));
     ASSERT_TRUE(merger.mergeFileOverlay(fileDesc, &fileB));
-
-    ResourceName name = test::parseNameOrDie(u"@com.app.a:xml/foo");
-    ResourceKeyRef key = { name, ConfigDescription{} };
-    auto iter = merger.getFilesToMerge().find(key);
-    ASSERT_NE(merger.getFilesToMerge().end(), iter);
-
-    const FileToMerge& actualFileToMerge = iter->second;
-    EXPECT_EQ(&fileB, actualFileToMerge.file);
 }
 
 TEST_F(TableMergerTest, MergeFileReferences) {
@@ -157,15 +139,6 @@
     f = test::getValue<FileReference>(&finalTable, u"@com.app.a:xml/com.app.b$file");
     ASSERT_NE(f, nullptr);
     EXPECT_EQ(std::u16string(u"res/xml/com.app.b$file.xml"), *f->path);
-
-    ResourceName name = test::parseNameOrDie(u"@com.app.a:xml/com.app.b$file");
-    ResourceKeyRef key = { name, ConfigDescription{} };
-    auto iter = merger.getFilesToMerge().find(key);
-    ASSERT_NE(merger.getFilesToMerge().end(), iter);
-
-    const FileToMerge& actualFileToMerge = iter->second;
-    EXPECT_EQ(Source("res/xml/file.xml"), actualFileToMerge.file->getSource());
-    EXPECT_EQ(std::string("res/xml/com.app.b$file.xml"), actualFileToMerge.dstPath);
 }
 
 TEST_F(TableMergerTest, OverrideResourceWithOverlay) {
@@ -244,36 +217,4 @@
     ASSERT_FALSE(merger.mergeOverlay({}, tableB.get()));
 }
 
-TEST_F(TableMergerTest, MergeAndStripResourcesNotMatchingFilter) {
-    ResourceTable finalTable;
-    TableMergerOptions options;
-    options.autoAddOverlay = false;
-
-    AxisConfigFilter filter;
-    filter.addConfig(test::parseConfigOrDie("en"));
-    options.filter = &filter;
-
-    test::TestFile fileA("res/layout-en/main.xml"), fileB("res/layout-fr-rFR/main.xml");
-    const ResourceName name = test::parseNameOrDie(u"@com.app.a:layout/main");
-    const ConfigDescription configEn = test::parseConfigOrDie("en");
-    const ConfigDescription configFr = test::parseConfigOrDie("fr-rFR");
-
-    TableMerger merger(mContext.get(), &finalTable, options);
-    ASSERT_TRUE(merger.mergeFile(ResourceFile{ name, configEn }, &fileA));
-    ASSERT_TRUE(merger.mergeFile(ResourceFile{ name, configFr }, &fileB));
-
-    EXPECT_NE(nullptr, test::getValueForConfig<FileReference>(&finalTable,
-                                                              u"@com.app.a:layout/main",
-                                                              configEn));
-    EXPECT_EQ(nullptr, test::getValueForConfig<FileReference>(&finalTable,
-                                                              u"@com.app.a:layout/main",
-                                                              configFr));
-
-    EXPECT_NE(merger.getFilesToMerge().end(),
-              merger.getFilesToMerge().find(ResourceKeyRef(name, configEn)));
-
-    EXPECT_EQ(merger.getFilesToMerge().end(),
-              merger.getFilesToMerge().find(ResourceKeyRef(name, configFr)));
-}
-
 } // namespace aapt
diff --git a/tools/aapt2/process/IResourceTableConsumer.h b/tools/aapt2/process/IResourceTableConsumer.h
index a2528d2..3a88044 100644
--- a/tools/aapt2/process/IResourceTableConsumer.h
+++ b/tools/aapt2/process/IResourceTableConsumer.h
@@ -40,6 +40,7 @@
     virtual StringPiece16 getCompilationPackage() = 0;
     virtual uint8_t getPackageId() = 0;
     virtual NameMangler* getNameMangler() = 0;
+    virtual bool verbose() = 0;
 };
 
 struct IResourceTableConsumer {
diff --git a/tools/aapt2/split/TableSplitter.cpp b/tools/aapt2/split/TableSplitter.cpp
new file mode 100644
index 0000000..0f7649b
--- /dev/null
+++ b/tools/aapt2/split/TableSplitter.cpp
@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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.
+ */
+
+#include "ConfigDescription.h"
+#include "ResourceTable.h"
+#include "split/TableSplitter.h"
+
+#include <map>
+#include <set>
+#include <unordered_map>
+#include <vector>
+
+namespace aapt {
+
+using ConfigClaimedMap = std::unordered_map<ResourceConfigValue*, bool>;
+using ConfigDensityGroups = std::map<ConfigDescription, std::vector<ResourceConfigValue*>>;
+
+static ConfigDescription copyWithoutDensity(const ConfigDescription& config) {
+    ConfigDescription withoutDensity = config;
+    withoutDensity.density = 0;
+    return withoutDensity;
+}
+
+/**
+ * Selects values that match exactly the constraints given.
+ */
+class SplitValueSelector {
+public:
+    SplitValueSelector(const SplitConstraints& constraints) {
+        for (const ConfigDescription& config : constraints.configs) {
+            if (config.density == 0) {
+                mDensityIndependentConfigs.insert(config);
+            } else {
+                mDensityDependentConfigToDensityMap[copyWithoutDensity(config)] = config.density;
+            }
+        }
+    }
+
+    std::vector<ResourceConfigValue*> selectValues(const ConfigDensityGroups& densityGroups,
+                                                   ConfigClaimedMap* claimedValues) {
+        std::vector<ResourceConfigValue*> selected;
+
+        // Select the regular values.
+        for (auto& entry : *claimedValues) {
+            // Check if the entry has a density.
+            ResourceConfigValue* configValue = entry.first;
+            if (configValue->config.density == 0 && !entry.second) {
+                // This is still available.
+                if (mDensityIndependentConfigs.find(configValue->config) !=
+                        mDensityIndependentConfigs.end()) {
+                    selected.push_back(configValue);
+
+                    // Mark the entry as taken.
+                    entry.second = true;
+                }
+            }
+        }
+
+        // Now examine the densities
+        for (auto& entry : densityGroups) {
+            // We do not care if the value is claimed, since density values can be
+            // in multiple splits.
+            const ConfigDescription& config = entry.first;
+            const std::vector<ResourceConfigValue*>& relatedValues = entry.second;
+
+            auto densityValueIter = mDensityDependentConfigToDensityMap.find(config);
+            if (densityValueIter != mDensityDependentConfigToDensityMap.end()) {
+                // Select the best one!
+                ConfigDescription targetDensity = config;
+                targetDensity.density = densityValueIter->second;
+
+                ResourceConfigValue* bestValue = nullptr;
+                for (ResourceConfigValue* thisValue : relatedValues) {
+                    if (!bestValue ||
+                            thisValue->config.isBetterThan(bestValue->config, &targetDensity)) {
+                        bestValue = thisValue;
+                    }
+
+                    // When we select one of these, they are all claimed such that the base
+                    // doesn't include any anymore.
+                    (*claimedValues)[thisValue] = true;
+                }
+                assert(bestValue);
+                selected.push_back(bestValue);
+            }
+        }
+        return selected;
+    }
+
+private:
+    std::set<ConfigDescription> mDensityIndependentConfigs;
+    std::map<ConfigDescription, uint16_t> mDensityDependentConfigToDensityMap;
+};
+
+/**
+ * Marking non-preferred densities as claimed will make sure the base doesn't include them,
+ * leaving only the preferred density behind.
+ */
+static void markNonPreferredDensitiesAsClaimed(uint16_t preferredDensity,
+                                               const ConfigDensityGroups& densityGroups,
+                                               ConfigClaimedMap* configClaimedMap) {
+    for (auto& entry : densityGroups) {
+        const ConfigDescription& config = entry.first;
+        const std::vector<ResourceConfigValue*>& relatedValues = entry.second;
+
+        ConfigDescription targetDensity = config;
+        targetDensity.density = preferredDensity;
+        ResourceConfigValue* bestValue = nullptr;
+        for (ResourceConfigValue* thisValue : relatedValues) {
+            if (!bestValue) {
+                bestValue = thisValue;
+            } else if (thisValue->config.isBetterThan(bestValue->config, &targetDensity)) {
+                // Claim the previous value so that it is not included in the base.
+                (*configClaimedMap)[bestValue] = true;
+                bestValue = thisValue;
+            } else {
+                // Claim this value so that it is not included in the base.
+                (*configClaimedMap)[thisValue] = true;
+            }
+        }
+        assert(bestValue);
+    }
+}
+
+bool TableSplitter::verifySplitConstraints(IAaptContext* context) {
+    bool error = false;
+    for (size_t i = 0; i < mSplitConstraints.size(); i++) {
+        for (size_t j = i + 1; j < mSplitConstraints.size(); j++) {
+            for (const ConfigDescription& config : mSplitConstraints[i].configs) {
+                if (mSplitConstraints[j].configs.find(config) !=
+                        mSplitConstraints[j].configs.end()) {
+                    context->getDiagnostics()->error(DiagMessage() << "config '" << config
+                                                     << "' appears in multiple splits, "
+                                                     << "target split ambiguous");
+                    error = true;
+                }
+            }
+        }
+    }
+    return !error;
+}
+
+void TableSplitter::splitTable(ResourceTable* originalTable) {
+    const size_t splitCount = mSplitConstraints.size();
+    for (auto& pkg : originalTable->packages) {
+        // Initialize all packages for splits.
+        for (size_t idx = 0; idx < splitCount; idx++) {
+            ResourceTable* splitTable = mSplits[idx].get();
+            splitTable->createPackage(pkg->name, pkg->id);
+        }
+
+        for (auto& type : pkg->types) {
+            if (type->type == ResourceType::kMipmap) {
+                // Always keep mipmaps.
+                continue;
+            }
+
+            for (auto& entry : type->entries) {
+                if (mConfigFilter) {
+                    // First eliminate any resource that we definitely don't want.
+                    for (std::unique_ptr<ResourceConfigValue>& configValue : entry->values) {
+                        if (!mConfigFilter->match(configValue->config)) {
+                            // null out the entry. We will clean up and remove nulls at the end
+                            // for performance reasons.
+                            configValue.reset();
+                        }
+                    }
+                }
+
+                // Organize the values into two separate buckets. Those that are density-dependent
+                // and those that are density-independent.
+                // One density technically matches all density, it's just that some densities
+                // match better. So we need to be aware of the full set of densities to make this
+                // decision.
+                ConfigDensityGroups densityGroups;
+                ConfigClaimedMap configClaimedMap;
+                for (const std::unique_ptr<ResourceConfigValue>& configValue : entry->values) {
+                    if (configValue) {
+                        configClaimedMap[configValue.get()] = false;
+
+                        if (configValue->config.density != 0) {
+                            // Create a bucket for this density-dependent config.
+                            densityGroups[copyWithoutDensity(configValue->config)]
+                                          .push_back(configValue.get());
+                        }
+                    }
+                }
+
+                // First we check all the splits. If it doesn't match one of the splits, we
+                // leave it in the base.
+                for (size_t idx = 0; idx < splitCount; idx++) {
+                    const SplitConstraints& splitConstraint = mSplitConstraints[idx];
+                    ResourceTable* splitTable = mSplits[idx].get();
+
+                    // Select the values we want from this entry for this split.
+                    SplitValueSelector selector(splitConstraint);
+                    std::vector<ResourceConfigValue*> selectedValues =
+                            selector.selectValues(densityGroups, &configClaimedMap);
+
+                    // No need to do any work if we selected nothing.
+                    if (!selectedValues.empty()) {
+                        // Create the same resource structure in the split. We do this lazily
+                        // because we might not have actual values for each type/entry.
+                        ResourceTablePackage* splitPkg = splitTable->findPackage(pkg->name);
+                        ResourceTableType* splitType = splitPkg->findOrCreateType(type->type);
+                        if (!splitType->id) {
+                            splitType->id = type->id;
+                            splitType->symbolStatus = type->symbolStatus;
+                        }
+
+                        ResourceEntry* splitEntry = splitType->findOrCreateEntry(entry->name);
+                        if (!splitEntry->id) {
+                            splitEntry->id = entry->id;
+                            splitEntry->symbolStatus = entry->symbolStatus;
+                        }
+
+                        // Copy the selected values into the new Split Entry.
+                        for (ResourceConfigValue* configValue : selectedValues) {
+                            ResourceConfigValue* newConfigValue = splitEntry->findOrCreateValue(
+                                    configValue->config, configValue->product);
+                            newConfigValue->value = std::unique_ptr<Value>(
+                                    configValue->value->clone(&splitTable->stringPool));
+                        }
+                    }
+                }
+
+                if (mPreferredDensity) {
+                    markNonPreferredDensitiesAsClaimed(mPreferredDensity.value(),
+                                                       densityGroups,
+                                                       &configClaimedMap);
+                }
+
+                // All splits are handled, now check to see what wasn't claimed and remove
+                // whatever exists in other splits.
+                for (std::unique_ptr<ResourceConfigValue>& configValue : entry->values) {
+                    if (configValue && configClaimedMap[configValue.get()]) {
+                        // Claimed, remove from base.
+                        configValue.reset();
+                    }
+                }
+
+                // Now erase all nullptrs.
+                entry->values.erase(
+                        std::remove(entry->values.begin(), entry->values.end(), nullptr),
+                        entry->values.end());
+            }
+        }
+    }
+}
+
+} // namespace aapt
diff --git a/tools/aapt2/split/TableSplitter.h b/tools/aapt2/split/TableSplitter.h
new file mode 100644
index 0000000..15e0764
--- /dev/null
+++ b/tools/aapt2/split/TableSplitter.h
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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.
+ */
+
+#ifndef AAPT_SPLIT_TABLESPLITTER_H
+#define AAPT_SPLIT_TABLESPLITTER_H
+
+#include "ConfigDescription.h"
+#include "ResourceTable.h"
+#include "filter/ConfigFilter.h"
+#include "process/IResourceTableConsumer.h"
+
+#include <android-base/macros.h>
+#include <set>
+#include <vector>
+
+namespace aapt {
+
+struct SplitConstraints {
+    std::set<ConfigDescription> configs;
+};
+
+struct TableSplitterOptions {
+    /**
+     * The preferred density to keep in the table, stripping out all others.
+     */
+    Maybe<uint16_t> preferredDensity;
+
+    /**
+     * Configuration filter that determines which resource configuration values end up in
+     * the final table.
+     */
+    IConfigFilter* configFilter = nullptr;
+};
+
+class TableSplitter {
+public:
+    TableSplitter(const std::vector<SplitConstraints>& splits,
+                  const TableSplitterOptions& options) :
+            mSplitConstraints(splits), mPreferredDensity(options.preferredDensity),
+            mConfigFilter(options.configFilter) {
+        for (size_t i = 0; i < mSplitConstraints.size(); i++) {
+            mSplits.push_back(util::make_unique<ResourceTable>());
+        }
+    }
+
+    bool verifySplitConstraints(IAaptContext* context);
+
+    void splitTable(ResourceTable* originalTable);
+
+    const std::vector<std::unique_ptr<ResourceTable>>& getSplits() {
+        return mSplits;
+    }
+
+private:
+    std::vector<SplitConstraints> mSplitConstraints;
+    std::vector<std::unique_ptr<ResourceTable>> mSplits;
+    Maybe<uint16_t> mPreferredDensity;
+    IConfigFilter* mConfigFilter;
+
+    DISALLOW_COPY_AND_ASSIGN(TableSplitter);
+};
+
+}
+
+#endif /* AAPT_SPLIT_TABLESPLITTER_H */
diff --git a/tools/aapt2/split/TableSplitter_test.cpp b/tools/aapt2/split/TableSplitter_test.cpp
new file mode 100644
index 0000000..74ca32e
--- /dev/null
+++ b/tools/aapt2/split/TableSplitter_test.cpp
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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.
+ */
+
+#include "split/TableSplitter.h"
+#include "test/Builders.h"
+#include "test/Common.h"
+
+#include <gtest/gtest.h>
+
+namespace aapt {
+
+TEST(TableSplitterTest, NoSplitPreferredDensity) {
+    std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder()
+            .addFileReference(u"@android:drawable/icon", u"res/drawable-mdpi/icon.png",
+                              test::parseConfigOrDie("mdpi"))
+            .addFileReference(u"@android:drawable/icon", u"res/drawable-hdpi/icon.png",
+                              test::parseConfigOrDie("hdpi"))
+            .addFileReference(u"@android:drawable/icon", u"res/drawable-xhdpi/icon.png",
+                              test::parseConfigOrDie("xhdpi"))
+            .addFileReference(u"@android:drawable/icon", u"res/drawable-xxhdpi/icon.png",
+                              test::parseConfigOrDie("xxhdpi"))
+            .addSimple(u"@android:string/one", {})
+            .build();
+
+    TableSplitterOptions options;
+    options.preferredDensity = ConfigDescription::DENSITY_XHIGH;
+    TableSplitter splitter({}, options);
+    splitter.splitTable(table.get());
+
+    EXPECT_EQ(nullptr, test::getValueForConfig<FileReference>(table.get(),
+                                                              u"@android:drawable/icon",
+                                                              test::parseConfigOrDie("mdpi")));
+    EXPECT_EQ(nullptr, test::getValueForConfig<FileReference>(table.get(),
+                                                              u"@android:drawable/icon",
+                                                              test::parseConfigOrDie("hdpi")));
+    EXPECT_NE(nullptr, test::getValueForConfig<FileReference>(table.get(),
+                                                              u"@android:drawable/icon",
+                                                              test::parseConfigOrDie("xhdpi")));
+    EXPECT_EQ(nullptr, test::getValueForConfig<FileReference>(table.get(),
+                                                              u"@android:drawable/icon",
+                                                              test::parseConfigOrDie("xxhdpi")));
+    EXPECT_NE(nullptr, test::getValue<Id>(table.get(), u"@android:string/one"));
+}
+
+TEST(TableSplitterTest, SplitTableByConfigAndDensity) {
+    ResourceTable table;
+
+    const ResourceName foo = test::parseNameOrDie(u"@android:string/foo");
+    ASSERT_TRUE(table.addResource(foo, test::parseConfigOrDie("land-hdpi"), {},
+                                  util::make_unique<Id>(),
+                                  test::getDiagnostics()));
+    ASSERT_TRUE(table.addResource(foo, test::parseConfigOrDie("land-xhdpi"), {},
+                                  util::make_unique<Id>(),
+                                  test::getDiagnostics()));
+    ASSERT_TRUE(table.addResource(foo, test::parseConfigOrDie("land-xxhdpi"), {},
+                                  util::make_unique<Id>(),
+                                  test::getDiagnostics()));
+
+    std::vector<SplitConstraints> constraints;
+    constraints.push_back(SplitConstraints{ { test::parseConfigOrDie("land-mdpi") } });
+    constraints.push_back(SplitConstraints{ { test::parseConfigOrDie("land-xhdpi") } });
+
+    TableSplitter splitter(constraints, TableSplitterOptions{});
+    splitter.splitTable(&table);
+
+    ASSERT_EQ(2u, splitter.getSplits().size());
+
+    ResourceTable* splitOne = splitter.getSplits()[0].get();
+    ResourceTable* splitTwo = splitter.getSplits()[1].get();
+
+    // Since a split was defined, all densities should be gone from base.
+    EXPECT_EQ(nullptr, test::getValueForConfig<Id>(&table, u"@android:string/foo",
+                                                   test::parseConfigOrDie("land-hdpi")));
+    EXPECT_EQ(nullptr, test::getValueForConfig<Id>(&table, u"@android:string/foo",
+                                                   test::parseConfigOrDie("land-xhdpi")));
+    EXPECT_EQ(nullptr, test::getValueForConfig<Id>(&table, u"@android:string/foo",
+                                                   test::parseConfigOrDie("land-xxhdpi")));
+
+    EXPECT_NE(nullptr, test::getValueForConfig<Id>(splitOne, u"@android:string/foo",
+                                                   test::parseConfigOrDie("land-hdpi")));
+    EXPECT_EQ(nullptr, test::getValueForConfig<Id>(splitOne, u"@android:string/foo",
+                                                   test::parseConfigOrDie("land-xhdpi")));
+    EXPECT_EQ(nullptr, test::getValueForConfig<Id>(splitOne, u"@android:string/foo",
+                                                   test::parseConfigOrDie("land-xxhdpi")));
+
+    EXPECT_EQ(nullptr, test::getValueForConfig<Id>(splitTwo, u"@android:string/foo",
+                                                   test::parseConfigOrDie("land-hdpi")));
+    EXPECT_NE(nullptr, test::getValueForConfig<Id>(splitTwo, u"@android:string/foo",
+                                                   test::parseConfigOrDie("land-xhdpi")));
+    EXPECT_EQ(nullptr, test::getValueForConfig<Id>(splitTwo, u"@android:string/foo",
+                                                   test::parseConfigOrDie("land-xxhdpi")));
+}
+
+} // namespace aapt
diff --git a/tools/aapt2/test/Context.h b/tools/aapt2/test/Context.h
index 555a539..e540cd7 100644
--- a/tools/aapt2/test/Context.h
+++ b/tools/aapt2/test/Context.h
@@ -71,6 +71,10 @@
         assert(mNameMangler && "test name mangler not set");
         return mNameMangler.get();
     }
+
+    bool verbose() override {
+        return false;
+    }
 };
 
 class ContextBuilder {