AAPT2: Expose split support to command line

Bug:30445078
Change-Id: If4b8530dba71b9059b8e62c04757da99c1119d22
diff --git a/tools/aapt2/AppInfo.h b/tools/aapt2/AppInfo.h
index 1d39b72..a9794a4 100644
--- a/tools/aapt2/AppInfo.h
+++ b/tools/aapt2/AppInfo.h
@@ -37,6 +37,16 @@
      * The App's minimum SDK version.
      */
     Maybe<std::string> minSdkVersion;
+
+    /**
+     * The Version code of the app.
+     */
+    Maybe<uint32_t> versionCode;
+
+    /**
+     * The revision code of the app.
+     */
+    Maybe<uint32_t> revisionCode;
 };
 
 } // namespace aapt
diff --git a/tools/aapt2/Main.cpp b/tools/aapt2/Main.cpp
index a74b5aa..ed55f85 100644
--- a/tools/aapt2/Main.cpp
+++ b/tools/aapt2/Main.cpp
@@ -25,7 +25,7 @@
 static const char* sMajorVersion = "2";
 
 // Update minor version whenever a feature or flag is added.
-static const char* sMinorVersion = "0";
+static const char* sMinorVersion = "1";
 
 int printVersion() {
     std::cerr << "Android Asset Packaging Tool (aapt) "
diff --git a/tools/aapt2/ResourceParser.cpp b/tools/aapt2/ResourceParser.cpp
index a144c6a..bcdf401 100644
--- a/tools/aapt2/ResourceParser.cpp
+++ b/tools/aapt2/ResourceParser.cpp
@@ -500,8 +500,8 @@
     };
 
     // Process the raw value.
-    std::unique_ptr<Item> processedItem = ResourceUtils::parseItemForAttribute(rawValue, typeMask,
-                                                                               onCreateReference);
+    std::unique_ptr<Item> processedItem = ResourceUtils::tryParseItemForAttribute(
+            rawValue, typeMask, onCreateReference);
     if (processedItem) {
         // Fix up the reference.
         if (Reference* ref = valueCast<Reference>(processedItem.get())) {
@@ -528,20 +528,24 @@
 bool ResourceParser::parseString(xml::XmlPullParser* parser, ParsedResource* outResource) {
     bool formatted = true;
     if (Maybe<StringPiece> formattedAttr = xml::findAttribute(parser, "formatted")) {
-        if (!ResourceUtils::tryParseBool(formattedAttr.value(), &formatted)) {
+        Maybe<bool> maybeFormatted = ResourceUtils::parseBool(formattedAttr.value());
+        if (!maybeFormatted) {
             mDiag->error(DiagMessage(outResource->source)
                          << "invalid value for 'formatted'. Must be a boolean");
             return false;
         }
+        formatted = maybeFormatted.value();
     }
 
     bool translateable = mOptions.translatable;
     if (Maybe<StringPiece> translateableAttr = xml::findAttribute(parser, "translatable")) {
-        if (!ResourceUtils::tryParseBool(translateableAttr.value(), &translateable)) {
+        Maybe<bool> maybeTranslateable = ResourceUtils::parseBool(translateableAttr.value());
+        if (!maybeTranslateable) {
             mDiag->error(DiagMessage(outResource->source)
                          << "invalid value for 'translatable'. Must be a boolean");
             return false;
         }
+        translateable = maybeTranslateable.value();
     }
 
     outResource->value = parseXml(parser, android::ResTable_map::TYPE_STRING, kNoRawString);
@@ -590,7 +594,7 @@
     outResource->name.type = *parsedType;
 
     if (Maybe<StringPiece> maybeIdStr = xml::findNonEmptyAttribute(parser, "id")) {
-        Maybe<ResourceId> maybeId = ResourceUtils::tryParseResourceId(maybeIdStr.value());
+        Maybe<ResourceId> maybeId = ResourceUtils::parseResourceId(maybeIdStr.value());
         if (!maybeId) {
             mDiag->error(DiagMessage(outResource->source)
                          << "invalid resource ID '" << maybeId.value() << "' in <public>");
@@ -630,7 +634,7 @@
         return false;
     }
 
-    Maybe<ResourceId> maybeId = ResourceUtils::tryParseResourceId(maybeIdStr.value());
+    Maybe<ResourceId> maybeId = ResourceUtils::parseResourceId(maybeIdStr.value());
     if (!maybeId) {
         mDiag->error(DiagMessage(outResource->source)
                      << "invalid resource ID '" << maybeIdStr.value() << "' in <public-group>");
@@ -1058,14 +1062,17 @@
 
     bool translateable = mOptions.translatable;
     if (Maybe<StringPiece> translateableAttr = xml::findAttribute(parser, "translatable")) {
-        if (!ResourceUtils::tryParseBool(translateableAttr.value(), &translateable)) {
+        Maybe<bool> maybeTranslateable = ResourceUtils::parseBool(translateableAttr.value());
+        if (!maybeTranslateable) {
             mDiag->error(DiagMessage(outResource->source)
                          << "invalid value for 'translatable'. Must be a boolean");
             return false;
         }
+        translateable = maybeTranslateable.value();
     }
     array->setTranslateable(translateable);
 
+
     bool error = false;
     const size_t depth = parser->getDepth();
     while (xml::XmlPullParser::nextChildNode(parser, depth)) {
diff --git a/tools/aapt2/ResourceUtils.cpp b/tools/aapt2/ResourceUtils.cpp
index 7dc88ded..f806d80 100644
--- a/tools/aapt2/ResourceUtils.cpp
+++ b/tools/aapt2/ResourceUtils.cpp
@@ -124,7 +124,7 @@
     return true;
 }
 
-bool tryParseReference(const StringPiece& str, ResourceNameRef* outRef, bool* outCreate,
+bool parseReference(const StringPiece& str, ResourceNameRef* outRef, bool* outCreate,
                        bool* outPrivate) {
     StringPiece trimmedStr(util::trimWhitespace(str));
     if (trimmedStr.empty()) {
@@ -171,10 +171,10 @@
 }
 
 bool isReference(const StringPiece& str) {
-    return tryParseReference(str, nullptr, nullptr, nullptr);
+    return parseReference(str, nullptr, nullptr, nullptr);
 }
 
-bool tryParseAttributeReference(const StringPiece& str, ResourceNameRef* outRef) {
+bool parseAttributeReference(const StringPiece& str, ResourceNameRef* outRef) {
     StringPiece trimmedStr(util::trimWhitespace(str));
     if (trimmedStr.empty()) {
         return false;
@@ -208,7 +208,7 @@
 }
 
 bool isAttributeReference(const StringPiece& str) {
-    return tryParseAttributeReference(str, nullptr);
+    return parseAttributeReference(str, nullptr);
 }
 
 /*
@@ -271,13 +271,13 @@
 std::unique_ptr<Reference> tryParseReference(const StringPiece& str, bool* outCreate) {
     ResourceNameRef ref;
     bool privateRef = false;
-    if (tryParseReference(str, &ref, outCreate, &privateRef)) {
+    if (parseReference(str, &ref, outCreate, &privateRef)) {
         std::unique_ptr<Reference> value = util::make_unique<Reference>(ref);
         value->privateReference = privateRef;
         return value;
     }
 
-    if (tryParseAttributeReference(str, &ref)) {
+    if (parseAttributeReference(str, &ref)) {
         if (outCreate) {
             *outCreate = false;
         }
@@ -420,23 +420,26 @@
     return error ? std::unique_ptr<BinaryPrimitive>() : util::make_unique<BinaryPrimitive>(value);
 }
 
-bool tryParseBool(const StringPiece& str, bool* outValue) {
+Maybe<bool> parseBool(const StringPiece& str) {
     StringPiece trimmedStr(util::trimWhitespace(str));
     if (trimmedStr == "true" || trimmedStr == "TRUE" || trimmedStr == "True") {
-        if (outValue) {
-            *outValue = true;
-        }
-        return true;
+        return Maybe<bool>(true);
     } else if (trimmedStr == "false" || trimmedStr == "FALSE" || trimmedStr == "False") {
-        if (outValue) {
-            *outValue = false;
-        }
-        return true;
+        return Maybe<bool>(false);
     }
-    return false;
+    return {};
 }
 
-Maybe<ResourceId> tryParseResourceId(const StringPiece& str) {
+Maybe<uint32_t> parseInt(const StringPiece& str) {
+    std::u16string str16 = util::utf8ToUtf16(str);
+    android::Res_value value;
+    if (android::ResTable::stringToInt(str16.data(), str16.size(), &value)) {
+        return value.data;
+    }
+    return {};
+}
+
+Maybe<ResourceId> parseResourceId(const StringPiece& str) {
     StringPiece trimmedStr(util::trimWhitespace(str));
 
     std::u16string str16 = util::utf8ToUtf16(trimmedStr);
@@ -452,7 +455,7 @@
     return {};
 }
 
-Maybe<int> tryParseSdkVersion(const StringPiece& str) {
+Maybe<int> parseSdkVersion(const StringPiece& str) {
     StringPiece trimmedStr(util::trimWhitespace(str));
 
     std::u16string str16 = util::utf8ToUtf16(trimmedStr);
@@ -470,12 +473,11 @@
 }
 
 std::unique_ptr<BinaryPrimitive> tryParseBool(const StringPiece& str) {
-    bool result = false;
-    if (tryParseBool(str, &result)) {
+    if (Maybe<bool> maybeResult = parseBool(str)) {
         android::Res_value value = {};
         value.dataType = android::Res_value::TYPE_INT_BOOLEAN;
 
-        if (result) {
+        if (maybeResult.value()) {
             value.data = 0xffffffffu;
         } else {
             value.data = 0;
@@ -542,7 +544,7 @@
     };
 }
 
-std::unique_ptr<Item> parseItemForAttribute(
+std::unique_ptr<Item> tryParseItemForAttribute(
         const StringPiece& value,
         uint32_t typeMask,
         std::function<void(const ResourceName&)> onCreateReference) {
@@ -602,11 +604,11 @@
  * We successively try to parse the string as a resource type that the Attribute
  * allows.
  */
-std::unique_ptr<Item> parseItemForAttribute(
+std::unique_ptr<Item> tryParseItemForAttribute(
         const StringPiece& str, const Attribute* attr,
         std::function<void(const ResourceName&)> onCreateReference) {
     const uint32_t typeMask = attr->typeMask;
-    std::unique_ptr<Item> value = parseItemForAttribute(str, typeMask, onCreateReference);
+    std::unique_ptr<Item> value = tryParseItemForAttribute(str, typeMask, onCreateReference);
     if (value) {
         return value;
     }
diff --git a/tools/aapt2/ResourceUtils.h b/tools/aapt2/ResourceUtils.h
index 31b8e89..00a1390 100644
--- a/tools/aapt2/ResourceUtils.h
+++ b/tools/aapt2/ResourceUtils.h
@@ -28,11 +28,6 @@
 namespace aapt {
 namespace ResourceUtils {
 
-/**
- * Convert an android::ResTable::resource_name to an aapt::ResourceName struct.
- */
-Maybe<ResourceName> toResourceName(const android::ResTable::resource_name& name);
-
 /*
  * Extracts the package, type, and name from a string of the format:
  *
@@ -60,8 +55,8 @@
  * If '+' was present in the reference, `outCreate` is set to true.
  * If '*' was present in the reference, `outPrivate` is set to true.
  */
-bool tryParseReference(const StringPiece& str, ResourceNameRef* outReference,
-                       bool* outCreate = nullptr, bool* outPrivate = nullptr);
+bool parseReference(const StringPiece& str, ResourceNameRef* outReference,
+                    bool* outCreate = nullptr, bool* outPrivate = nullptr);
 
 /*
  * Returns true if the string is in the form of a resource reference (@[+][package:]type/name).
@@ -72,7 +67,7 @@
  * Returns true if the string was parsed as an attribute reference (?[package:][type/]name),
  * with `outReference` set to the parsed reference.
  */
-bool tryParseAttributeReference(const StringPiece& str, ResourceNameRef* outReference);
+bool parseAttributeReference(const StringPiece& str, ResourceNameRef* outReference);
 
 /**
  * Returns true if the string is in the form of an attribute reference(?[package:][type/]name).
@@ -80,19 +75,29 @@
 bool isAttributeReference(const StringPiece& str);
 
 /**
- * Returns true if the value is a boolean, putting the result in `outValue`.
+ * Convert an android::ResTable::resource_name to an aapt::ResourceName struct.
  */
-bool tryParseBool(const StringPiece& str, bool* outValue);
+Maybe<ResourceName> toResourceName(const android::ResTable::resource_name& name);
+
+/**
+ * Returns a boolean value if the string is equal to TRUE, true, True, FALSE, false, or False.
+ */
+Maybe<bool> parseBool(const StringPiece& str);
+
+/**
+ * Returns a uint32_t if the string is an integer.
+ */
+Maybe<uint32_t> parseInt(const StringPiece& str);
 
 /**
  * Returns an ID if it the string represented a valid ID.
  */
-Maybe<ResourceId> tryParseResourceId(const StringPiece& str);
+Maybe<ResourceId> parseResourceId(const StringPiece& str);
 
 /**
  * Parses an SDK version, which can be an integer, or a letter from A-Z.
  */
-Maybe<int> tryParseSdkVersion(const StringPiece& str);
+Maybe<int> parseSdkVersion(const StringPiece& str);
 
 /*
  * Returns a Reference, or None Maybe instance if the string `str` was parsed as a
@@ -161,11 +166,11 @@
  * The callback function onCreateReference is called when the parsed item is a
  * reference to an ID that must be created (@+id/foo).
  */
-std::unique_ptr<Item> parseItemForAttribute(
+std::unique_ptr<Item> tryParseItemForAttribute(
         const StringPiece& value, const Attribute* attr,
         std::function<void(const ResourceName&)> onCreateReference = {});
 
-std::unique_ptr<Item> parseItemForAttribute(
+std::unique_ptr<Item> tryParseItemForAttribute(
         const StringPiece& value, uint32_t typeMask,
         std::function<void(const ResourceName&)> onCreateReference = {});
 
diff --git a/tools/aapt2/ResourceUtils_test.cpp b/tools/aapt2/ResourceUtils_test.cpp
index fb76914..894cfcf 100644
--- a/tools/aapt2/ResourceUtils_test.cpp
+++ b/tools/aapt2/ResourceUtils_test.cpp
@@ -21,24 +21,12 @@
 namespace aapt {
 
 TEST(ResourceUtilsTest, ParseBool) {
-    bool val = false;
-    EXPECT_TRUE(ResourceUtils::tryParseBool("true", &val));
-    EXPECT_TRUE(val);
-
-    EXPECT_TRUE(ResourceUtils::tryParseBool("TRUE", &val));
-    EXPECT_TRUE(val);
-
-    EXPECT_TRUE(ResourceUtils::tryParseBool("True", &val));
-    EXPECT_TRUE(val);
-
-    EXPECT_TRUE(ResourceUtils::tryParseBool("false", &val));
-    EXPECT_FALSE(val);
-
-    EXPECT_TRUE(ResourceUtils::tryParseBool("FALSE", &val));
-    EXPECT_FALSE(val);
-
-    EXPECT_TRUE(ResourceUtils::tryParseBool("False", &val));
-    EXPECT_FALSE(val);
+    EXPECT_EQ(Maybe<bool>(true), ResourceUtils::parseBool("true"));
+    EXPECT_EQ(Maybe<bool>(true), ResourceUtils::parseBool("TRUE"));
+    EXPECT_EQ(Maybe<bool>(true), ResourceUtils::parseBool("True"));
+    EXPECT_EQ(Maybe<bool>(false), ResourceUtils::parseBool("false"));
+    EXPECT_EQ(Maybe<bool>(false), ResourceUtils::parseBool("FALSE"));
+    EXPECT_EQ(Maybe<bool>(false), ResourceUtils::parseBool("False"));
 }
 
 TEST(ResourceUtilsTest, ParseResourceName) {
@@ -64,7 +52,7 @@
     ResourceNameRef actual;
     bool create = false;
     bool privateRef = false;
-    EXPECT_TRUE(ResourceUtils::tryParseReference("@color/foo", &actual, &create, &privateRef));
+    EXPECT_TRUE(ResourceUtils::parseReference("@color/foo", &actual, &create, &privateRef));
     EXPECT_EQ(expected, actual);
     EXPECT_FALSE(create);
     EXPECT_FALSE(privateRef);
@@ -75,7 +63,7 @@
     ResourceNameRef actual;
     bool create = false;
     bool privateRef = false;
-    EXPECT_TRUE(ResourceUtils::tryParseReference("@android:color/foo", &actual, &create,
+    EXPECT_TRUE(ResourceUtils::parseReference("@android:color/foo", &actual, &create,
                                                  &privateRef));
     EXPECT_EQ(expected, actual);
     EXPECT_FALSE(create);
@@ -87,7 +75,7 @@
     ResourceNameRef actual;
     bool create = false;
     bool privateRef = false;
-    EXPECT_TRUE(ResourceUtils::tryParseReference("\t @android:color/foo\n \n\t", &actual,
+    EXPECT_TRUE(ResourceUtils::parseReference("\t @android:color/foo\n \n\t", &actual,
                                                  &create, &privateRef));
     EXPECT_EQ(expected, actual);
     EXPECT_FALSE(create);
@@ -99,7 +87,7 @@
     ResourceNameRef actual;
     bool create = false;
     bool privateRef = false;
-    EXPECT_TRUE(ResourceUtils::tryParseReference("@+android:id/foo", &actual, &create,
+    EXPECT_TRUE(ResourceUtils::parseReference("@+android:id/foo", &actual, &create,
                                                  &privateRef));
     EXPECT_EQ(expected, actual);
     EXPECT_TRUE(create);
@@ -111,7 +99,7 @@
     ResourceNameRef actual;
     bool create = false;
     bool privateRef = false;
-    EXPECT_TRUE(ResourceUtils::tryParseReference("@*android:id/foo", &actual, &create,
+    EXPECT_TRUE(ResourceUtils::parseReference("@*android:id/foo", &actual, &create,
                                                  &privateRef));
     EXPECT_EQ(expected, actual);
     EXPECT_FALSE(create);
@@ -122,7 +110,7 @@
     bool create = false;
     bool privateRef = false;
     ResourceNameRef actual;
-    EXPECT_FALSE(ResourceUtils::tryParseReference("@+android:color/foo", &actual, &create,
+    EXPECT_FALSE(ResourceUtils::parseReference("@+android:color/foo", &actual, &create,
                                                   &privateRef));
 }
 
diff --git a/tools/aapt2/ResourceValues.cpp b/tools/aapt2/ResourceValues.cpp
index 4a86579..73682ab 100644
--- a/tools/aapt2/ResourceValues.cpp
+++ b/tools/aapt2/ResourceValues.cpp
@@ -449,9 +449,7 @@
     printMask(out);
 
     if (!symbols.empty()) {
-        *out << " ["
-            << util::joiner(symbols.begin(), symbols.end(), ", ")
-            << "]";
+        *out << " [" << util::joiner(symbols, ", ") << "]";
     }
 
     if (minInt != std::numeric_limits<int32_t>::min()) {
@@ -600,7 +598,7 @@
         *out << parent.value().name.value();
     }
     *out << " ["
-        << util::joiner(entries.begin(), entries.end(), ", ")
+        << util::joiner(entries, ", ")
         << "]";
 }
 
@@ -645,7 +643,7 @@
 
 void Array::print(std::ostream* out) const {
     *out << "(array) ["
-        << util::joiner(items.begin(), items.end(), ", ")
+        << util::joiner(items, ", ")
         << "]";
 }
 
@@ -730,7 +728,7 @@
 
 void Styleable::print(std::ostream* out) const {
     *out << "(styleable) " << " ["
-        << util::joiner(entries.begin(), entries.end(), ", ")
+        << util::joiner(entries, ", ")
         << "]";
 }
 
diff --git a/tools/aapt2/compile/XmlIdCollector.cpp b/tools/aapt2/compile/XmlIdCollector.cpp
index 9fc979c..3901419 100644
--- a/tools/aapt2/compile/XmlIdCollector.cpp
+++ b/tools/aapt2/compile/XmlIdCollector.cpp
@@ -42,7 +42,7 @@
         for (xml::Attribute& attr : element->attributes) {
             ResourceNameRef name;
             bool create = false;
-            if (ResourceUtils::tryParseReference(attr.value, &name, &create, nullptr)) {
+            if (ResourceUtils::parseReference(attr.value, &name, &create, nullptr)) {
                 if (create && name.type == ResourceType::kId) {
                     auto iter = std::lower_bound(mOutSymbols->begin(), mOutSymbols->end(),
                                                  name, cmpName);
diff --git a/tools/aapt2/link/Link.cpp b/tools/aapt2/link/Link.cpp
index acb0f38..c1c5ba2 100644
--- a/tools/aapt2/link/Link.cpp
+++ b/tools/aapt2/link/Link.cpp
@@ -59,11 +59,14 @@
     std::string manifestPath;
     std::vector<std::string> includePaths;
     std::vector<std::string> overlayFiles;
+
+    // Java/Proguard options.
     Maybe<std::string> generateJavaClassPath;
     Maybe<std::string> customJavaPackage;
     std::set<std::string> extraJavaPackages;
     Maybe<std::string> generateProguardRulesPath;
     Maybe<std::string> generateMainDexProguardRulesPath;
+
     bool noAutoVersion = false;
     bool noVersionVectors = false;
     bool staticLib = false;
@@ -77,7 +80,13 @@
     Maybe<std::string> privateSymbols;
     ManifestFixerOptions manifestFixerOptions;
     std::unordered_set<std::string> products;
+
+    // Split APK options.
     TableSplitterOptions tableSplitterOptions;
+    std::vector<SplitConstraints> splitConstraints;
+    std::vector<std::string> splitPaths;
+
+    // Stable ID options.
     std::unordered_map<ResourceName, ResourceId> stableIdMap;
     Maybe<std::string> resourceIdMapPath;
 };
@@ -585,7 +594,7 @@
         const size_t resIdStrLen = line.size() - resIdStartIdx;
         StringPiece resIdStr = util::trimWhitespace(line.substr(resIdStartIdx, resIdStrLen));
 
-        Maybe<ResourceId> maybeId = ResourceUtils::tryParseResourceId(resIdStr);
+        Maybe<ResourceId> maybeId = ResourceUtils::parseResourceId(resIdStr);
         if (!maybeId) {
             diag->error(DiagMessage(Source(path, lineNo)) << "invalid resource ID '"
                         << resIdStr << "'");
@@ -597,6 +606,28 @@
     return true;
 }
 
+static bool parseSplitParameter(const StringPiece& arg, IDiagnostics* diag,
+                                std::string* outPath, SplitConstraints* outSplit) {
+    std::vector<std::string> parts = util::split(arg, ':');
+    if (parts.size() != 2) {
+        diag->error(DiagMessage() << "invalid split parameter '" << arg << "'");
+        diag->note(DiagMessage() << "should be --split path/to/output.apk:<config>[,<config>...]");
+        return false;
+    }
+    *outPath = parts[0];
+    std::vector<ConfigDescription> configs;
+    for (const StringPiece& configStr : util::tokenize(parts[1], ',')) {
+        configs.push_back({});
+        if (!ConfigDescription::parse(configStr, &configs.back())) {
+            diag->error(DiagMessage() << "invalid config '" << configStr
+                        << "' in split parameter '" << arg << "'");
+            return false;
+        }
+    }
+    outSplit->configs.insert(configs.begin(), configs.end());
+    return true;
+}
+
 class LinkCommand {
 public:
     LinkCommand(LinkContext* context, const LinkOptions& options) :
@@ -676,6 +707,30 @@
 
             appInfo.package = packageAttr->value;
 
+            if (xml::Attribute* versionCodeAttr =
+                    manifestEl->findAttribute(xml::kSchemaAndroid, "versionCode")) {
+                Maybe<uint32_t> maybeCode = ResourceUtils::parseInt(versionCodeAttr->value);
+                if (!maybeCode) {
+                    diag->error(DiagMessage(xmlRes->file.source.withLine(manifestEl->lineNumber))
+                                << "invalid android:versionCode '"
+                                << versionCodeAttr->value << "'");
+                    return {};
+                }
+                appInfo.versionCode = maybeCode.value();
+            }
+
+            if (xml::Attribute* revisionCodeAttr =
+                    manifestEl->findAttribute(xml::kSchemaAndroid, "revisionCode")) {
+                Maybe<uint32_t> maybeCode = ResourceUtils::parseInt(revisionCodeAttr->value);
+                if (!maybeCode) {
+                    diag->error(DiagMessage(xmlRes->file.source.withLine(manifestEl->lineNumber))
+                                << "invalid android:revisionCode '"
+                                << revisionCodeAttr->value << "'");
+                    return {};
+                }
+                appInfo.revisionCode = maybeCode.value();
+            }
+
             if (xml::Element* usesSdkEl = manifestEl->findChild({}, "uses-sdk")) {
                 if (xml::Attribute* minSdk =
                         usesSdkEl->findAttribute(xml::kSchemaAndroid, "minSdkVersion")) {
@@ -767,11 +822,11 @@
         return true;
     }
 
-    std::unique_ptr<IArchiveWriter> makeArchiveWriter() {
+    std::unique_ptr<IArchiveWriter> makeArchiveWriter(const StringPiece& out) {
         if (mOptions.outputToDirectory) {
-            return createDirectoryArchiveWriter(mContext->getDiagnostics(), mOptions.outputPath);
+            return createDirectoryArchiveWriter(mContext->getDiagnostics(), out);
         } else {
-            return createZipFileArchiveWriter(mContext->getDiagnostics(), mOptions.outputPath);
+            return createZipFileArchiveWriter(mContext->getDiagnostics(), out);
         }
     }
 
@@ -1179,6 +1234,94 @@
         return true;
     }
 
+    std::unique_ptr<xml::XmlResource> generateSplitManifest(const AppInfo& appInfo,
+                                                            const SplitConstraints& constraints) {
+        std::unique_ptr<xml::XmlResource> doc = util::make_unique<xml::XmlResource>();
+
+        std::unique_ptr<xml::Namespace> namespaceAndroid = util::make_unique<xml::Namespace>();
+        namespaceAndroid->namespaceUri = xml::kSchemaAndroid;
+        namespaceAndroid->namespacePrefix = "android";
+
+        std::unique_ptr<xml::Element> manifestEl = util::make_unique<xml::Element>();
+        manifestEl->name = "manifest";
+        manifestEl->attributes.push_back(
+                xml::Attribute{ "", "package", appInfo.package });
+
+        if (appInfo.versionCode) {
+            manifestEl->attributes.push_back(xml::Attribute{
+                    xml::kSchemaAndroid,
+                    "versionCode",
+                    std::to_string(appInfo.versionCode.value()) });
+        }
+
+        if (appInfo.revisionCode) {
+            manifestEl->attributes.push_back(xml::Attribute{
+                    xml::kSchemaAndroid,
+                    "revisionCode", std::to_string(appInfo.revisionCode.value()) });
+        }
+
+        std::stringstream splitName;
+        splitName << "config." << util::joiner(constraints.configs, "_");
+
+        manifestEl->attributes.push_back(
+                xml::Attribute{ "", "split", splitName.str() });
+
+        std::unique_ptr<xml::Element> applicationEl = util::make_unique<xml::Element>();
+        applicationEl->name = "application";
+        applicationEl->attributes.push_back(
+                xml::Attribute{ xml::kSchemaAndroid, "hasCode", "false" });
+
+        manifestEl->addChild(std::move(applicationEl));
+        namespaceAndroid->addChild(std::move(manifestEl));
+        doc->root = std::move(namespaceAndroid);
+        return doc;
+    }
+
+    /**
+     * Writes the AndroidManifest, ResourceTable, and all XML files referenced by the ResourceTable
+     * to the IArchiveWriter.
+     */
+    bool writeApk(IArchiveWriter* writer, proguard::KeepSet* keepSet, xml::XmlResource* manifest,
+                  ResourceTable* table) {
+        const bool keepRawValues = mOptions.staticLib;
+        bool result = flattenXml(manifest, "AndroidManifest.xml", {}, keepRawValues, writer,
+                                 mContext);
+        if (!result) {
+            return false;
+        }
+
+        ResourceFileFlattenerOptions fileFlattenerOptions;
+        fileFlattenerOptions.keepRawValues = keepRawValues;
+        fileFlattenerOptions.doNotCompressAnything = mOptions.doNotCompressAnything;
+        fileFlattenerOptions.extensionsToNotCompress = mOptions.extensionsToNotCompress;
+        fileFlattenerOptions.noAutoVersion = mOptions.noAutoVersion;
+        fileFlattenerOptions.noVersionVectors = mOptions.noVersionVectors;
+        fileFlattenerOptions.updateProguardSpec =
+                static_cast<bool>(mOptions.generateProguardRulesPath);
+
+        ResourceFileFlattener fileFlattener(fileFlattenerOptions, mContext, keepSet);
+
+        if (!fileFlattener.flatten(table, writer)) {
+            mContext->getDiagnostics()->error(DiagMessage() << "failed linking file resources");
+            return false;
+        }
+
+        if (mOptions.staticLib) {
+            if (!flattenTableToPb(table, writer)) {
+                mContext->getDiagnostics()->error(DiagMessage()
+                                                  << "failed to write resources.arsc.flat");
+                return false;
+            }
+        } else {
+            if (!flattenTable(table, writer)) {
+                mContext->getDiagnostics()->error(DiagMessage()
+                                                  << "failed to write resources.arsc");
+                return false;
+            }
+        }
+        return true;
+    }
+
     int run(const std::vector<std::string>& inputFiles) {
         // Load the AndroidManifest.xml
         std::unique_ptr<xml::XmlResource> manifestXml = loadXml(mOptions.manifestPath,
@@ -1187,30 +1330,33 @@
             return 1;
         }
 
+        // First extract the Package name without modifying it (via --rename-manifest-package).
         if (Maybe<AppInfo> maybeAppInfo = extractAppInfoFromManifest(manifestXml.get(),
                                                                      mContext->getDiagnostics())) {
-            AppInfo& appInfo = maybeAppInfo.value();
+            const AppInfo& appInfo = maybeAppInfo.value();
             mContext->setCompilationPackage(appInfo.package);
-            if (appInfo.minSdkVersion) {
-                if (Maybe<int> maybeMinSdkVersion =
-                        ResourceUtils::tryParseSdkVersion(appInfo.minSdkVersion.value())) {
-                    mContext->setMinSdkVersion(maybeMinSdkVersion.value());
-                }
-            }
-        } else {
+        }
+
+        ManifestFixer manifestFixer(mOptions.manifestFixerOptions);
+        if (!manifestFixer.consume(mContext, manifestXml.get())) {
             return 1;
         }
 
-        if (!util::isJavaPackageName(mContext->getCompilationPackage())) {
-            mContext->getDiagnostics()->error(DiagMessage(mOptions.manifestPath)
-                                             << "invalid package name '"
-                                             << mContext->getCompilationPackage()
-                                             << "'");
+        Maybe<AppInfo> maybeAppInfo = extractAppInfoFromManifest(manifestXml.get(),
+                                                                 mContext->getDiagnostics());
+        if (!maybeAppInfo) {
             return 1;
         }
 
+        const AppInfo& appInfo = maybeAppInfo.value();
+        if (appInfo.minSdkVersion) {
+            if (Maybe<int> maybeMinSdkVersion =
+                    ResourceUtils::parseSdkVersion(appInfo.minSdkVersion.value())) {
+                mContext->setMinSdkVersion(maybeMinSdkVersion.value());
+            }
+        }
+
         mContext->setNameManglerPolicy(NameManglerPolicy{ mContext->getCompilationPackage() });
-
         if (mContext->getCompilationPackage() == "android") {
             mContext->setPackageId(0x01);
         } else {
@@ -1258,9 +1404,7 @@
                         DiagMessage() << "failed moving private attributes");
                 return 1;
             }
-        }
 
-        if (!mOptions.staticLib) {
             // Assign IDs if we are building a regular app.
             IdAssigner idAssigner(&mOptions.stableIdMap);
             if (!idAssigner.consume(mContext, &mFinalTable)) {
@@ -1304,45 +1448,118 @@
         mContext->getExternalSymbols()->prependSource(
                         util::make_unique<ResourceTableSymbolSource>(&mFinalTable));
 
-        {
-            ReferenceLinker linker;
-            if (!linker.consume(mContext, &mFinalTable)) {
-                mContext->getDiagnostics()->error(DiagMessage() << "failed linking references");
+        ReferenceLinker linker;
+        if (!linker.consume(mContext, &mFinalTable)) {
+            mContext->getDiagnostics()->error(DiagMessage() << "failed linking references");
+            return 1;
+        }
+
+        if (mOptions.staticLib) {
+            if (!mOptions.products.empty()) {
+                mContext->getDiagnostics()->warn(
+                        DiagMessage() << "can't select products when building static library");
+            }
+        } else {
+            ProductFilter productFilter(mOptions.products);
+            if (!productFilter.consume(mContext, &mFinalTable)) {
+                mContext->getDiagnostics()->error(DiagMessage() << "failed stripping products");
                 return 1;
             }
+        }
 
-            if (mOptions.staticLib) {
-                if (!mOptions.products.empty()) {
-                    mContext->getDiagnostics()->warn(
-                            DiagMessage() << "can't select products when building static library");
-                }
+        if (!mOptions.noAutoVersion) {
+            AutoVersioner versioner;
+            if (!versioner.consume(mContext, &mFinalTable)) {
+                mContext->getDiagnostics()->error(DiagMessage() << "failed versioning styles");
+                return 1;
+            }
+        }
 
-                if (mOptions.tableSplitterOptions.configFilter != nullptr ||
-                        mOptions.tableSplitterOptions.preferredDensity) {
-                    mContext->getDiagnostics()->warn(
-                            DiagMessage() << "can't strip resources when building static library");
-                }
-            } else {
-                ProductFilter productFilter(mOptions.products);
-                if (!productFilter.consume(mContext, &mFinalTable)) {
-                    mContext->getDiagnostics()->error(DiagMessage() << "failed stripping products");
-                    return 1;
-                }
+        if (!mOptions.staticLib && mContext->getMinSdkVersion() > 0) {
+            if (mContext->verbose()) {
+                mContext->getDiagnostics()->note(
+                        DiagMessage() << "collapsing resource versions for minimum SDK "
+                        << mContext->getMinSdkVersion());
+            }
 
-                // 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);
+            VersionCollapser collapser;
+            if (!collapser.consume(mContext, &mFinalTable)) {
+                return 1;
             }
         }
 
         proguard::KeepSet proguardKeepSet;
         proguard::KeepSet proguardMainDexKeepSet;
 
-        std::unique_ptr<IArchiveWriter> archiveWriter = makeArchiveWriter();
+        if (mOptions.staticLib) {
+            if (mOptions.tableSplitterOptions.configFilter != nullptr ||
+                    mOptions.tableSplitterOptions.preferredDensity) {
+                mContext->getDiagnostics()->warn(
+                        DiagMessage() << "can't strip resources when building static library");
+            }
+        } else {
+            // Adjust the SplitConstraints so that their SDK version is stripped if it is less
+            // than or equal to the minSdk. Otherwise the resources that have had their SDK version
+            // stripped due to minSdk won't ever match.
+            std::vector<SplitConstraints> adjustedConstraintsList;
+            adjustedConstraintsList.reserve(mOptions.splitConstraints.size());
+            for (const SplitConstraints& constraints : mOptions.splitConstraints) {
+                SplitConstraints adjustedConstraints;
+                for (const ConfigDescription& config : constraints.configs) {
+                    if (config.sdkVersion <= mContext->getMinSdkVersion()) {
+                        adjustedConstraints.configs.insert(config.copyWithoutSdkVersion());
+                    } else {
+                        adjustedConstraints.configs.insert(config);
+                    }
+                }
+                adjustedConstraintsList.push_back(std::move(adjustedConstraints));
+            }
+
+            TableSplitter tableSplitter(adjustedConstraintsList, mOptions.tableSplitterOptions);
+            if (!tableSplitter.verifySplitConstraints(mContext)) {
+                return 1;
+            }
+            tableSplitter.splitTable(&mFinalTable);
+
+            // Now we need to write out the Split APKs.
+            auto pathIter = mOptions.splitPaths.begin();
+            auto splitConstraintsIter = adjustedConstraintsList.begin();
+            for (std::unique_ptr<ResourceTable>& splitTable : tableSplitter.getSplits()) {
+                if (mContext->verbose()) {
+                    mContext->getDiagnostics()->note(
+                            DiagMessage(*pathIter) << "generating split with configurations '"
+                            << util::joiner(splitConstraintsIter->configs, ", ") << "'");
+                }
+
+                std::unique_ptr<IArchiveWriter> archiveWriter = makeArchiveWriter(*pathIter);
+                if (!archiveWriter) {
+                    mContext->getDiagnostics()->error(DiagMessage() << "failed to create archive");
+                    return 1;
+                }
+
+                // Generate an AndroidManifest.xml for each split.
+                std::unique_ptr<xml::XmlResource> splitManifest =
+                        generateSplitManifest(appInfo, *splitConstraintsIter);
+
+                XmlReferenceLinker linker;
+                if (!linker.consume(mContext, splitManifest.get())) {
+                    mContext->getDiagnostics()->error(
+                            DiagMessage() << "failed to create Split AndroidManifest.xml");
+                    return 1;
+                }
+
+                if (!writeApk(archiveWriter.get(), &proguardKeepSet, splitManifest.get(),
+                              splitTable.get())) {
+                    return 1;
+                }
+
+                ++pathIter;
+                ++splitConstraintsIter;
+            }
+        }
+
+        // Start writing the base APK.
+        std::unique_ptr<IArchiveWriter> archiveWriter = makeArchiveWriter(mOptions.outputPath);
         if (!archiveWriter) {
             mContext->getDiagnostics()->error(DiagMessage() << "failed to create archive");
             return 1;
@@ -1350,11 +1567,6 @@
 
         bool error = false;
         {
-            ManifestFixer manifestFixer(mOptions.manifestFixerOptions);
-            if (!manifestFixer.consume(mContext, manifestXml.get())) {
-                error = true;
-            }
-
             // AndroidManifest.xml has no resource name, but the CallSite is built from the name
             // (aka, which package the AndroidManifest.xml is coming from).
             // So we give it a package name so it can see local resources.
@@ -1382,13 +1594,6 @@
                         error = true;
                     }
                 }
-
-                const bool keepRawValues = mOptions.staticLib;
-                bool result = flattenXml(manifestXml.get(), "AndroidManifest.xml", {},
-                                         keepRawValues, archiveWriter.get(), mContext);
-                if (!result) {
-                    error = true;
-                }
             } else {
                 error = true;
             }
@@ -1399,58 +1604,10 @@
             return 1;
         }
 
-        if (!mOptions.noAutoVersion) {
-            AutoVersioner versioner;
-            if (!versioner.consume(mContext, &mFinalTable)) {
-                mContext->getDiagnostics()->error(DiagMessage() << "failed versioning styles");
-                return 1;
-            }
-        }
-
-        if (!mOptions.staticLib && mContext->getMinSdkVersion() > 0) {
-            if (mContext->verbose()) {
-                mContext->getDiagnostics()->note(
-                        DiagMessage() << "collapsing resource versions for minimum SDK "
-                        << mContext->getMinSdkVersion());
-            }
-
-            VersionCollapser collapser;
-            if (!collapser.consume(mContext, &mFinalTable)) {
-                return 1;
-            }
-        }
-
-        // Write out the table to an archive. Optimizations to the table should come before this
-        // step.
-        ResourceFileFlattenerOptions fileFlattenerOptions;
-        fileFlattenerOptions.keepRawValues = mOptions.staticLib;
-        fileFlattenerOptions.doNotCompressAnything = mOptions.doNotCompressAnything;
-        fileFlattenerOptions.extensionsToNotCompress = mOptions.extensionsToNotCompress;
-        fileFlattenerOptions.noAutoVersion = mOptions.noAutoVersion;
-        fileFlattenerOptions.noVersionVectors = mOptions.noVersionVectors;
-        fileFlattenerOptions.updateProguardSpec =
-                static_cast<bool>(mOptions.generateProguardRulesPath);
-        ResourceFileFlattener fileFlattener(fileFlattenerOptions, mContext, &proguardKeepSet);
-
-        if (!fileFlattener.flatten(&mFinalTable, archiveWriter.get())) {
-            mContext->getDiagnostics()->error(DiagMessage() << "failed linking file resources");
+        if (!writeApk(archiveWriter.get(), &proguardKeepSet, manifestXml.get(), &mFinalTable)) {
             return 1;
         }
 
-        if (mOptions.staticLib) {
-            if (!flattenTableToPb(&mFinalTable, archiveWriter.get())) {
-                mContext->getDiagnostics()->error(DiagMessage()
-                                                  << "failed to write resources.arsc.flat");
-                return 1;
-            }
-        } else {
-            if (!flattenTable(&mFinalTable, archiveWriter.get())) {
-                mContext->getDiagnostics()->error(DiagMessage()
-                                                  << "failed to write resources.arsc");
-                return 1;
-            }
-        }
-
         if (mOptions.generateJavaClassPath) {
             JavaClassGeneratorOptions options;
             options.types = JavaClassGeneratorOptions::SymbolTypes::kAll;
@@ -1538,6 +1695,7 @@
     bool requireLocalization = false;
     bool verbose = false;
     Maybe<std::string> stableIdFilePath;
+    std::vector<std::string> splitArgs;
     Flags flags = Flags()
             .requiredFlag("-o", "Output path", &options.outputPath)
             .requiredFlag("--manifest", "Path to the Android manifest to build",
@@ -1623,6 +1781,9 @@
                           &options.manifestFixerOptions.renameInstrumentationTargetPackage)
             .optionalFlagList("-0", "File extensions not to compress",
                               &options.extensionsToNotCompress)
+            .optionalFlagList("--split", "Split resources matching a set of configs out to a "
+                              "Split APK.\nSyntax: path/to/output.apk:<config>[,<config>[...]]",
+                              &splitArgs)
             .optionalSwitch("-v", "Enables verbose logging",
                             &verbose);
 
@@ -1741,6 +1902,16 @@
             ".m4v", ".3gp", ".3gpp", ".3g2", ".3gpp2",
             ".amr", ".awb", ".wma", ".wmv", ".webm", ".mkv"});
 
+    // Parse the split parameters.
+    for (const std::string& splitArg : splitArgs) {
+        options.splitPaths.push_back({});
+        options.splitConstraints.push_back({});
+        if (!parseSplitParameter(splitArg, context.getDiagnostics(), &options.splitPaths.back(),
+                                 &options.splitConstraints.back())) {
+            return 1;
+        }
+    }
+
     // Turn off auto versioning for static-libs.
     if (options.staticLib) {
         options.noAutoVersion = true;
diff --git a/tools/aapt2/link/ReferenceLinker.cpp b/tools/aapt2/link/ReferenceLinker.cpp
index 6aa9c0e..be7aca3 100644
--- a/tools/aapt2/link/ReferenceLinker.cpp
+++ b/tools/aapt2/link/ReferenceLinker.cpp
@@ -138,7 +138,7 @@
                                                   const Attribute* attr) {
         if (RawString* rawString = valueCast<RawString>(value.get())) {
             std::unique_ptr<Item> transformed =
-                    ResourceUtils::parseItemForAttribute(*rawString->value, attr);
+                    ResourceUtils::tryParseItemForAttribute(*rawString->value, attr);
 
             // If we could not parse as any specific type, try a basic STRING.
             if (!transformed && (attr->typeMask & android::ResTable_map::TYPE_STRING)) {
diff --git a/tools/aapt2/link/XmlReferenceLinker.cpp b/tools/aapt2/link/XmlReferenceLinker.cpp
index a29d8dc..59ffe15 100644
--- a/tools/aapt2/link/XmlReferenceLinker.cpp
+++ b/tools/aapt2/link/XmlReferenceLinker.cpp
@@ -106,7 +106,7 @@
                     }
 
                     const Attribute* attribute = &attr.compiledAttribute.value().attribute;
-                    attr.compiledValue = ResourceUtils::parseItemForAttribute(attr.value,
+                    attr.compiledValue = ResourceUtils::tryParseItemForAttribute(attr.value,
                                                                               attribute);
                     if (!attr.compiledValue &&
                             !(attribute->typeMask & android::ResTable_map::TYPE_STRING)) {
diff --git a/tools/aapt2/readme.md b/tools/aapt2/readme.md
index 95c0173..a68d6f6 100644
--- a/tools/aapt2/readme.md
+++ b/tools/aapt2/readme.md
@@ -1,5 +1,14 @@
 # Android Asset Packaging Tool 2.0 (AAPT2) release notes
 
+## Version 2.1
+### `aapt2 link ...`
+- Configuration Split APK support: supports splitting resources that match a set of
+  configurations to a separate APK which can be loaded alongside the base APK on
+  API 21+ devices. This is done using the flag
+  `--split path/to/split.apk:<config1>[,<config2>,...]`.
+- SDK version resource filtering: Resources with an SDK version qualifier that is unreachable
+  at runtime due to the minimum SDK level declared by the AndroidManifest.xml are stripped.
+
 ## Version 2.0
 ### `aapt2 compile ...`
 - Pseudo-localization: generates pseudolocalized versions of default strings when the
diff --git a/tools/aapt2/split/TableSplitter.cpp b/tools/aapt2/split/TableSplitter.cpp
index 2dfe2a2..08b9ee9 100644
--- a/tools/aapt2/split/TableSplitter.cpp
+++ b/tools/aapt2/split/TableSplitter.cpp
@@ -17,6 +17,7 @@
 #include "ConfigDescription.h"
 #include "ResourceTable.h"
 #include "split/TableSplitter.h"
+#include "util/Util.h"
 
 #include <algorithm>
 #include <map>
@@ -76,7 +77,6 @@
             // 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!
@@ -89,12 +89,12 @@
                             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);
+
+                // When we select one of these, they are all claimed such that the base
+                // doesn't include any anymore.
+                (*claimedValues)[bestValue] = true;
                 selected.push_back(bestValue);
             }
         }
@@ -135,7 +135,6 @@
         assert(bestValue);
     }
 }
-
 bool TableSplitter::verifySplitConstraints(IAaptContext* context) {
     bool error = false;
     for (size_t i = 0; i < mSplitConstraints.size(); i++) {
diff --git a/tools/aapt2/split/TableSplitter.h b/tools/aapt2/split/TableSplitter.h
index 15e0764..2fa5c47 100644
--- a/tools/aapt2/split/TableSplitter.h
+++ b/tools/aapt2/split/TableSplitter.h
@@ -60,7 +60,7 @@
 
     void splitTable(ResourceTable* originalTable);
 
-    const std::vector<std::unique_ptr<ResourceTable>>& getSplits() {
+    std::vector<std::unique_ptr<ResourceTable>>& getSplits() {
         return mSplits;
     }
 
diff --git a/tools/aapt2/split/TableSplitter_test.cpp b/tools/aapt2/split/TableSplitter_test.cpp
index bad02a5..5150e82 100644
--- a/tools/aapt2/split/TableSplitter_test.cpp
+++ b/tools/aapt2/split/TableSplitter_test.cpp
@@ -52,6 +52,71 @@
     EXPECT_NE(nullptr, test::getValue<Id>(table.get(), "android:string/one"));
 }
 
+TEST(TableSplitterTest, SplitTableByDensity) {
+    std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder()
+            .addFileReference("android:drawable/foo", "res/drawable-mdpi/foo.png",
+                              test::parseConfigOrDie("mdpi"))
+            .addFileReference("android:drawable/foo", "res/drawable-hdpi/foo.png",
+                              test::parseConfigOrDie("hdpi"))
+            .addFileReference("android:drawable/foo", "res/drawable-xhdpi/foo.png",
+                              test::parseConfigOrDie("xhdpi"))
+            .addFileReference("android:drawable/foo", "res/drawable-xxhdpi/foo.png",
+                              test::parseConfigOrDie("xxhdpi"))
+            .build();
+
+    std::vector<SplitConstraints> constraints;
+    constraints.push_back(SplitConstraints{ { test::parseConfigOrDie("mdpi") } });
+    constraints.push_back(SplitConstraints{ { test::parseConfigOrDie("hdpi") } });
+    constraints.push_back(SplitConstraints{ { test::parseConfigOrDie("xhdpi") } });
+
+    TableSplitter splitter(constraints, TableSplitterOptions{});
+    splitter.splitTable(table.get());
+
+    ASSERT_EQ(3u, splitter.getSplits().size());
+
+    ResourceTable* splitOne = splitter.getSplits()[0].get();
+    ResourceTable* splitTwo = splitter.getSplits()[1].get();
+    ResourceTable* splitThree = splitter.getSplits()[2].get();
+
+    // Just xxhdpi should be in the base.
+    EXPECT_EQ(nullptr, test::getValueForConfig<FileReference>(table.get(), "android:drawable/foo",
+                                                              test::parseConfigOrDie("mdpi")));
+    EXPECT_EQ(nullptr, test::getValueForConfig<FileReference>(table.get(), "android:drawable/foo",
+                                                              test::parseConfigOrDie("hdpi")));
+    EXPECT_EQ(nullptr, test::getValueForConfig<FileReference>(table.get(), "android:drawable/foo",
+                                                              test::parseConfigOrDie("xhdpi")));
+    EXPECT_NE(nullptr, test::getValueForConfig<FileReference>(table.get(), "android:drawable/foo",
+                                                              test::parseConfigOrDie("xxhdpi")));
+
+    // Each split should have one and only one drawable.
+    EXPECT_NE(nullptr, test::getValueForConfig<FileReference>(splitOne, "android:drawable/foo",
+                                                              test::parseConfigOrDie("mdpi")));
+    EXPECT_EQ(nullptr, test::getValueForConfig<FileReference>(splitOne, "android:drawable/foo",
+                                                              test::parseConfigOrDie("hdpi")));
+    EXPECT_EQ(nullptr, test::getValueForConfig<FileReference>(splitOne, "android:drawable/foo",
+                                                              test::parseConfigOrDie("xhdpi")));
+    EXPECT_EQ(nullptr, test::getValueForConfig<FileReference>(splitOne, "android:drawable/foo",
+                                                              test::parseConfigOrDie("xxhdpi")));
+
+    EXPECT_EQ(nullptr, test::getValueForConfig<FileReference>(splitTwo, "android:drawable/foo",
+                                                              test::parseConfigOrDie("mdpi")));
+    EXPECT_NE(nullptr, test::getValueForConfig<FileReference>(splitTwo, "android:drawable/foo",
+                                                              test::parseConfigOrDie("hdpi")));
+    EXPECT_EQ(nullptr, test::getValueForConfig<FileReference>(splitTwo, "android:drawable/foo",
+                                                              test::parseConfigOrDie("xhdpi")));
+    EXPECT_EQ(nullptr, test::getValueForConfig<FileReference>(splitTwo, "android:drawable/foo",
+                                                              test::parseConfigOrDie("xxhdpi")));
+
+    EXPECT_EQ(nullptr, test::getValueForConfig<FileReference>(splitThree, "android:drawable/foo",
+                                                              test::parseConfigOrDie("mdpi")));
+    EXPECT_EQ(nullptr, test::getValueForConfig<FileReference>(splitThree, "android:drawable/foo",
+                                                              test::parseConfigOrDie("hdpi")));
+    EXPECT_NE(nullptr, test::getValueForConfig<FileReference>(splitThree, "android:drawable/foo",
+                                                              test::parseConfigOrDie("xhdpi")));
+    EXPECT_EQ(nullptr, test::getValueForConfig<FileReference>(splitThree, "android:drawable/foo",
+                                                              test::parseConfigOrDie("xxhdpi")));
+}
+
 TEST(TableSplitterTest, SplitTableByConfigAndDensity) {
     ResourceTable table;
 
@@ -78,12 +143,12 @@
     ResourceTable* splitOne = splitter.getSplits()[0].get();
     ResourceTable* splitTwo = splitter.getSplits()[1].get();
 
-    // Since a split was defined, all densities should be gone from base.
+    // All but the xxhdpi resource should be gone, since there were closer matches in land-xhdpi.
     EXPECT_EQ(nullptr, test::getValueForConfig<Id>(&table, "android:string/foo",
                                                    test::parseConfigOrDie("land-hdpi")));
     EXPECT_EQ(nullptr, test::getValueForConfig<Id>(&table, "android:string/foo",
                                                    test::parseConfigOrDie("land-xhdpi")));
-    EXPECT_EQ(nullptr, test::getValueForConfig<Id>(&table, "android:string/foo",
+    EXPECT_NE(nullptr, test::getValueForConfig<Id>(&table, "android:string/foo",
                                                    test::parseConfigOrDie("land-xxhdpi")));
 
     EXPECT_NE(nullptr, test::getValueForConfig<Id>(splitOne, "android:string/foo",
diff --git a/tools/aapt2/util/Util.h b/tools/aapt2/util/Util.h
index 4a10987..09f9129 100644
--- a/tools/aapt2/util/Util.h
+++ b/tools/aapt2/util/Util.h
@@ -101,12 +101,16 @@
  * Writes a set of items to the std::ostream, joining the times with the provided
  * separator.
  */
-template <typename Iterator>
-::std::function<::std::ostream&(::std::ostream&)> joiner(Iterator begin, Iterator end,
-        const char* sep) {
-    return [begin, end, sep](::std::ostream& out) -> ::std::ostream& {
-        for (auto iter = begin; iter != end; ++iter) {
-            if (iter != begin) {
+template <typename Container>
+::std::function<::std::ostream&(::std::ostream&)> joiner(const Container& container,
+                                                         const char* sep) {
+    using std::begin;
+    using std::end;
+    const auto beginIter = begin(container);
+    const auto endIter = end(container);
+    return [beginIter, endIter, sep](::std::ostream& out) -> ::std::ostream& {
+        for (auto iter = beginIter; iter != endIter; ++iter) {
+            if (iter != beginIter) {
                 out << sep;
             }
             out << *iter;