AAPT2: Add a APK filtering.

Allow resource files to be removed from the final artifact based on the
density and locale configuration in the config file. The APK is split
along the density, locale and ABI axis. Each split is generated from the
original APK without modifying the original. The new resource table is
written back to the file system with unneeded assets etc removed.

Test: Unit tests
Test: Manually run optimize command against an APK and inspect results
Test: Installed split searchlite APK (after resigning) and ran on N6

Change-Id: If73597dcfd88c02d2616518585d0e25a5c6a84d1
diff --git a/tools/aapt2/Android.bp b/tools/aapt2/Android.bp
index 53794e6..15fb016 100644
--- a/tools/aapt2/Android.bp
+++ b/tools/aapt2/Android.bp
@@ -106,6 +106,7 @@
         "link/XmlCompatVersioner.cpp",
         "link/XmlNamespaceRemover.cpp",
         "link/XmlReferenceLinker.cpp",
+        "optimize/MultiApkGenerator.cpp",
         "optimize/ResourceDeduper.cpp",
         "optimize/VersionCollapser.cpp",
         "process/SymbolTable.cpp",
diff --git a/tools/aapt2/LoadedApk.cpp b/tools/aapt2/LoadedApk.cpp
index abc0e4c..b80780e 100644
--- a/tools/aapt2/LoadedApk.cpp
+++ b/tools/aapt2/LoadedApk.cpp
@@ -58,14 +58,15 @@
 bool LoadedApk::WriteToArchive(IAaptContext* context, const TableFlattenerOptions& options,
                                IArchiveWriter* writer) {
   FilterChain empty;
-  return WriteToArchive(context, options, &empty, writer);
+  return WriteToArchive(context, table_.get(), options, &empty, writer);
 }
 
-bool LoadedApk::WriteToArchive(IAaptContext* context, const TableFlattenerOptions& options,
-                               FilterChain* filters, IArchiveWriter* writer) {
+bool LoadedApk::WriteToArchive(IAaptContext* context, ResourceTable* split_table,
+                               const TableFlattenerOptions& options, FilterChain* filters,
+                               IArchiveWriter* writer) {
   std::set<std::string> referenced_resources;
   // List the files being referenced in the resource table.
-  for (auto& pkg : table_->packages) {
+  for (auto& pkg : split_table->packages) {
     for (auto& type : pkg->types) {
       for (auto& entry : type->entries) {
         for (auto& config_value : entry->values) {
@@ -108,7 +109,7 @@
       // TODO(adamlesinski): How to determine if there were sparse entries (and if to encode
       // with sparse entries) b/35389232.
       TableFlattener flattener(options, &buffer);
-      if (!flattener.Consume(context, table_.get())) {
+      if (!flattener.Consume(context, split_table)) {
         return false;
       }
 
diff --git a/tools/aapt2/LoadedApk.h b/tools/aapt2/LoadedApk.h
index 8aa9674..dacd0c2 100644
--- a/tools/aapt2/LoadedApk.h
+++ b/tools/aapt2/LoadedApk.h
@@ -47,16 +47,17 @@
    * Writes the APK on disk at the given path, while also removing the resource
    * files that are not referenced in the resource table.
    */
-  bool WriteToArchive(IAaptContext* context, const TableFlattenerOptions& options,
-                      IArchiveWriter* writer);
+  virtual bool WriteToArchive(IAaptContext* context, const TableFlattenerOptions& options,
+                              IArchiveWriter* writer);
 
   /**
    * Writes the APK on disk at the given path, while also removing the resource
    * files that are not referenced in the resource table. The provided filter
    * chain is applied to each entry in the APK file.
    */
-  bool WriteToArchive(IAaptContext* context, const TableFlattenerOptions& options,
-                      FilterChain* filters, IArchiveWriter* writer);
+  virtual bool WriteToArchive(IAaptContext* context, ResourceTable* split_table,
+                              const TableFlattenerOptions& options, FilterChain* filters,
+                              IArchiveWriter* writer);
 
   static std::unique_ptr<LoadedApk> LoadApkFromPath(IAaptContext* context,
                                                     const android::StringPiece& path);
diff --git a/tools/aapt2/ResourceTable.cpp b/tools/aapt2/ResourceTable.cpp
index ab59560..0304e21 100644
--- a/tools/aapt2/ResourceTable.cpp
+++ b/tools/aapt2/ResourceTable.cpp
@@ -546,4 +546,34 @@
   return SearchResult{package, type, entry};
 }
 
+std::unique_ptr<ResourceTable> ResourceTable::Clone() const {
+  std::unique_ptr<ResourceTable> new_table = util::make_unique<ResourceTable>();
+  for (const auto& pkg : packages) {
+    ResourceTablePackage* new_pkg = new_table->CreatePackage(pkg->name, pkg->id);
+    for (const auto& type : pkg->types) {
+      ResourceTableType* new_type = new_pkg->FindOrCreateType(type->type);
+      if (!new_type->id) {
+        new_type->id = type->id;
+        new_type->symbol_status = type->symbol_status;
+      }
+
+      for (const auto& entry : type->entries) {
+        ResourceEntry* new_entry = new_type->FindOrCreateEntry(entry->name);
+        if (!new_entry->id) {
+          new_entry->id = entry->id;
+          new_entry->symbol_status = entry->symbol_status;
+        }
+
+        for (const auto& config_value : entry->values) {
+          ResourceConfigValue* new_value =
+              new_entry->FindOrCreateValue(config_value->config, config_value->product);
+          Value* value = config_value->value->Clone(&new_table->string_pool);
+          new_value->value = std::unique_ptr<Value>(value);
+        }
+      }
+    }
+  }
+  return new_table;
+}
+
 }  // namespace aapt
diff --git a/tools/aapt2/ResourceTable.h b/tools/aapt2/ResourceTable.h
index 4295d06..d5db67e 100644
--- a/tools/aapt2/ResourceTable.h
+++ b/tools/aapt2/ResourceTable.h
@@ -251,6 +251,8 @@
 
   ResourceTablePackage* CreatePackage(const android::StringPiece& name, Maybe<uint8_t> id = {});
 
+  std::unique_ptr<ResourceTable> Clone() const;
+
   /**
    * The string pool used by this resource table. Values that reference strings
    * must use
diff --git a/tools/aapt2/cmd/Optimize.cpp b/tools/aapt2/cmd/Optimize.cpp
index 9d71775..84b7927 100644
--- a/tools/aapt2/cmd/Optimize.cpp
+++ b/tools/aapt2/cmd/Optimize.cpp
@@ -18,6 +18,7 @@
 #include <vector>
 
 #include "android-base/stringprintf.h"
+#include "androidfw/ResourceTypes.h"
 #include "androidfw/StringPiece.h"
 
 #include "Diagnostics.h"
@@ -33,15 +34,19 @@
 #include "flatten/XmlFlattener.h"
 #include "io/BigBufferInputStream.h"
 #include "io/Util.h"
+#include "optimize/MultiApkGenerator.h"
 #include "optimize/ResourceDeduper.h"
 #include "optimize/VersionCollapser.h"
 #include "split/TableSplitter.h"
 #include "util/Files.h"
+#include "util/Util.h"
 
 using ::aapt::configuration::Abi;
 using ::aapt::configuration::Artifact;
 using ::aapt::configuration::PostProcessingConfiguration;
+using ::android::ResTable_config;
 using ::android::StringPiece;
+using ::android::base::StringAppendF;
 using ::android::base::StringPrintf;
 
 namespace aapt {
@@ -188,42 +193,10 @@
     }
 
     if (options_.configuration && options_.output_dir) {
-      PostProcessingConfiguration& config = options_.configuration.value();
-
-      // For now, just write out the stripped APK since ABI splitting doesn't modify anything else.
-      for (const Artifact& artifact : config.artifacts) {
-        if (artifact.abi_group) {
-          const std::string& group = artifact.abi_group.value();
-
-          auto abi_group = config.abi_groups.find(group);
-          // TODO: Remove validation when configuration parser ensures referential integrity.
-          if (abi_group == config.abi_groups.end()) {
-            context_->GetDiagnostics()->Note(
-                DiagMessage() << "could not find referenced ABI group '" << group << "'");
-            return 1;
-          }
-          FilterChain filters;
-          filters.AddFilter(AbiFilter::FromAbiList(abi_group->second));
-
-          const std::string& path = apk->GetSource().path;
-          const StringPiece ext = file::GetExtension(path);
-          const std::string name = path.substr(0, path.rfind(ext.to_string()));
-
-          // Name is hard coded for now since only one split dimension is supported.
-          // TODO: Incorporate name generation into the configuration objects.
-          const std::string file_name =
-              StringPrintf("%s.%s%s", name.c_str(), group.c_str(), ext.data());
-          std::string out = options_.output_dir.value();
-          file::AppendPath(&out, file_name);
-
-          std::unique_ptr<IArchiveWriter> writer =
-              CreateZipFileArchiveWriter(context_->GetDiagnostics(), out);
-
-          if (!apk->WriteToArchive(context_, options_.table_flattener_options, &filters,
-                                   writer.get())) {
-            return 1;
-          }
-        }
+      MultiApkGenerator generator{apk.get(), context_};
+      if (!generator.FromBaseApk(options_.output_dir.value(), options_.configuration.value(),
+                                 options_.table_flattener_options)) {
+        return 1;
       }
     }
 
@@ -260,7 +233,7 @@
 
         for (auto& entry : type->entries) {
           for (auto& config_value : entry->values) {
-            FileReference* file_ref = ValueCast<FileReference>(config_value->value.get());
+            auto* file_ref = ValueCast<FileReference>(config_value->value.get());
             if (file_ref == nullptr) {
               continue;
             }
@@ -297,11 +270,8 @@
     }
 
     io::BigBufferInputStream table_buffer_in(&table_buffer);
-    if (!io::CopyInputStreamToArchive(context_, &table_buffer_in, "resources.arsc",
-                                      ArchiveEntry::kAlign, writer)) {
-      return false;
-    }
-    return true;
+    return io::CopyInputStreamToArchive(context_, &table_buffer_in, "resources.arsc",
+                                        ArchiveEntry::kAlign, writer);
   }
 
   OptimizeOptions options_;
@@ -349,6 +319,7 @@
   OptimizeOptions options;
   Maybe<std::string> config_path;
   Maybe<std::string> target_densities;
+  Maybe<std::string> target_abis;
   std::vector<std::string> configs;
   std::vector<std::string> split_args;
   bool verbose = false;
@@ -363,6 +334,12 @@
               "All the resources that would be unused on devices of the given densities will be \n"
               "removed from the APK.",
               &target_densities)
+          .OptionalFlag(
+              "--target-abis",
+              "Comma separated list of the CPU ABIs that the APK will be optimized for.\n"
+              "All the native libraries that would be unused on devices of the given ABIs will \n"
+              "be removed from the APK.",
+              &target_abis)
           .OptionalFlagList("-c",
                             "Comma separated list of configurations to include. The default\n"
                             "is all configurations.",
@@ -388,7 +365,8 @@
     return 1;
   }
 
-  std::unique_ptr<LoadedApk> apk = LoadedApk::LoadApkFromPath(&context, flags.GetArgs()[0]);
+  const std::string& apk_path = flags.GetArgs()[0];
+  std::unique_ptr<LoadedApk> apk = LoadedApk::LoadApkFromPath(&context, apk_path);
   if (!apk) {
     return 1;
   }
@@ -418,8 +396,8 @@
 
   // Parse the split parameters.
   for (const std::string& split_arg : split_args) {
-    options.split_paths.push_back({});
-    options.split_constraints.push_back({});
+    options.split_paths.emplace_back();
+    options.split_constraints.emplace_back();
     if (!ParseSplitParameter(split_arg, context.GetDiagnostics(), &options.split_paths.back(),
                              &options.split_constraints.back())) {
       return 1;
diff --git a/tools/aapt2/configuration/ConfigurationParser.cpp b/tools/aapt2/configuration/ConfigurationParser.cpp
index b0ed792..a9d6da0 100644
--- a/tools/aapt2/configuration/ConfigurationParser.cpp
+++ b/tools/aapt2/configuration/ConfigurationParser.cpp
@@ -59,6 +59,7 @@
 using ::aapt::xml::XmlActionExecutorPolicy;
 using ::aapt::xml::XmlNodeAction;
 using ::android::base::ReadFileToString;
+using ::android::StringPiece;
 
 const std::unordered_map<std::string, Abi> kStringToAbiMap = {
     {"armeabi", Abi::kArmeV6}, {"armeabi-v7a", Abi::kArmV7a},  {"arm64-v8a", Abi::kArm64V8a},
@@ -117,9 +118,9 @@
  * success, or false if the either the placeholder is not found in the name, or the value is not
  * present and the placeholder was.
  */
-static bool ReplacePlaceholder(const std::string& placeholder, const Maybe<std::string>& value,
+static bool ReplacePlaceholder(const StringPiece& placeholder, const Maybe<StringPiece>& value,
                                std::string* name, IDiagnostics* diag) {
-  size_t offset = name->find(placeholder);
+  size_t offset = name->find(placeholder.data());
   bool found = (offset != std::string::npos);
 
   // Make sure the placeholder was present if the desired value is present.
@@ -139,43 +140,83 @@
     return false;
   }
 
-  name->replace(offset, placeholder.length(), value.value());
+  name->replace(offset, placeholder.length(), value.value().data());
 
   // Make sure there was only one instance of the placeholder.
-  if (name->find(placeholder) != std::string::npos) {
+  if (name->find(placeholder.data()) != std::string::npos) {
     diag->Error(DiagMessage() << "Placeholder present multiple times: " << placeholder);
     return false;
   }
   return true;
 }
 
-Maybe<std::string> Artifact::ToArtifactName(const std::string& format, IDiagnostics* diag) const {
-  std::string result = format;
+Maybe<std::string> Artifact::ToArtifactName(const StringPiece& format, IDiagnostics* diag,
+                                            const StringPiece& base_name,
+                                            const StringPiece& ext) const {
+  std::string result = format.to_string();
 
-  if (!ReplacePlaceholder("{abi}", abi_group, &result, diag)) {
+  Maybe<StringPiece> maybe_base_name =
+      base_name.empty() ? Maybe<StringPiece>{} : Maybe<StringPiece>{base_name};
+  if (!ReplacePlaceholder("${basename}", maybe_base_name, &result, diag)) {
     return {};
   }
 
-  if (!ReplacePlaceholder("{density}", screen_density_group, &result, diag)) {
+  // Extension is optional.
+  if (result.find("${ext}") != std::string::npos) {
+    if (!ReplacePlaceholder("${ext}", {ext}, &result, diag)) {
+      return {};
+    }
+  }
+
+  if (!ReplacePlaceholder("${abi}", abi_group, &result, diag)) {
     return {};
   }
 
-  if (!ReplacePlaceholder("{locale}", locale_group, &result, diag)) {
+  if (!ReplacePlaceholder("${density}", screen_density_group, &result, diag)) {
     return {};
   }
 
-  if (!ReplacePlaceholder("{sdk}", android_sdk_group, &result, diag)) {
+  if (!ReplacePlaceholder("${locale}", locale_group, &result, diag)) {
     return {};
   }
 
-  if (!ReplacePlaceholder("{feature}", device_feature_group, &result, diag)) {
+  if (!ReplacePlaceholder("${sdk}", android_sdk_group, &result, diag)) {
     return {};
   }
 
-  if (!ReplacePlaceholder("{gl}", gl_texture_group, &result, diag)) {
+  if (!ReplacePlaceholder("${feature}", device_feature_group, &result, diag)) {
     return {};
   }
 
+  if (!ReplacePlaceholder("${gl}", gl_texture_group, &result, diag)) {
+    return {};
+  }
+
+  return result;
+}
+
+Maybe<std::string> Artifact::Name(const StringPiece& base_name, const StringPiece& ext,
+                                  IDiagnostics* diag) const {
+  if (!name) {
+    return {};
+  }
+
+  std::string result = name.value();
+
+  // Base name is optional.
+  if (result.find("${basename}") != std::string::npos) {
+    if (!ReplacePlaceholder("${basename}", {base_name}, &result, diag)) {
+      return {};
+    }
+  }
+
+  // Extension is optional.
+  if (result.find("${ext}") != std::string::npos) {
+    if (!ReplacePlaceholder("${ext}", {ext}, &result, diag)) {
+      return {};
+    }
+  }
+
   return result;
 }
 
@@ -346,7 +387,10 @@
         if ((t = NodeCast<xml::Text>(node.get())) != nullptr) {
           ConfigDescription config_descriptor;
           const android::StringPiece& text = TrimWhitespace(t->text);
-          if (ConfigDescription::Parse(text, &config_descriptor)) {
+          bool parsed = ConfigDescription::Parse(text, &config_descriptor);
+          if (parsed &&
+              (config_descriptor.CopyWithoutSdkVersion().diff(ConfigDescription::DefaultConfig()) ==
+               android::ResTable_config::CONFIG_DENSITY)) {
             // Copy the density with the minimum SDK version stripped out.
             group.push_back(config_descriptor.CopyWithoutSdkVersion());
           } else {
@@ -379,17 +423,25 @@
                                 << child->name);
       valid = false;
     } else {
-      Locale entry;
-      for (const auto& attr : child->attributes) {
-        if (attr.name == "lang") {
-          entry.lang = {attr.value};
-        } else if (attr.name == "region") {
-          entry.region = {attr.value};
-        } else {
-          diag->Warn(DiagMessage() << "Unknown attribute: " << attr.name << " = " << attr.value);
+      for (auto& node : child->children) {
+        xml::Text* t;
+        if ((t = NodeCast<xml::Text>(node.get())) != nullptr) {
+          ConfigDescription config_descriptor;
+          const android::StringPiece& text = TrimWhitespace(t->text);
+          bool parsed = ConfigDescription::Parse(text, &config_descriptor);
+          if (parsed &&
+              (config_descriptor.CopyWithoutSdkVersion().diff(ConfigDescription::DefaultConfig()) ==
+               android::ResTable_config::CONFIG_LOCALE)) {
+            // Copy the locale with the minimum SDK version stripped out.
+            group.push_back(config_descriptor.CopyWithoutSdkVersion());
+          } else {
+            diag->Error(DiagMessage()
+                        << "Could not parse config descriptor for screen-density: " << text);
+            valid = false;
+          }
+          break;
         }
       }
-      group.push_back(entry);
     }
   }
 
diff --git a/tools/aapt2/configuration/ConfigurationParser.h b/tools/aapt2/configuration/ConfigurationParser.h
index 28c355e..6259ce8 100644
--- a/tools/aapt2/configuration/ConfigurationParser.h
+++ b/tools/aapt2/configuration/ConfigurationParser.h
@@ -36,7 +36,7 @@
 /** Output artifact configuration options. */
 struct Artifact {
   /** Name to use for output of processing foo.apk -> foo.<name>.apk. */
-  std::string name;
+  Maybe<std::string> name;
   /** If present, uses the ABI group with this name. */
   Maybe<std::string> abi_group;
   /** If present, uses the screen density group with this name. */
@@ -51,7 +51,13 @@
   Maybe<std::string> gl_texture_group;
 
   /** Convert an artifact name template into a name string based on configuration contents. */
-  Maybe<std::string> ToArtifactName(const std::string& format, IDiagnostics* diag) const;
+  Maybe<std::string> ToArtifactName(const android::StringPiece& format, IDiagnostics* diag,
+                                    const android::StringPiece& base_name = "",
+                                    const android::StringPiece& ext = "apk") const;
+
+  /** Convert an artifact name template into a name string based on configuration contents. */
+  Maybe<std::string> Name(const android::StringPiece& base_name, const android::StringPiece& ext,
+                          IDiagnostics* diag) const;
 };
 
 /** Enumeration of currently supported ABIs. */
@@ -129,7 +135,7 @@
 
   Group<Abi> abi_groups;
   Group<ConfigDescription> screen_density_groups;
-  Group<Locale> locale_groups;
+  Group<ConfigDescription> locale_groups;
   Group<AndroidSdk> android_sdk_groups;
   Group<DeviceFeature> device_feature_groups;
   Group<GlTexture> gl_texture_groups;
diff --git a/tools/aapt2/configuration/ConfigurationParser_test.cpp b/tools/aapt2/configuration/ConfigurationParser_test.cpp
index ab3b7ec..5bd0831 100644
--- a/tools/aapt2/configuration/ConfigurationParser_test.cpp
+++ b/tools/aapt2/configuration/ConfigurationParser_test.cpp
@@ -67,18 +67,15 @@
       <screen-density>xxxhdpi</screen-density>
     </screen-density-group>
     <locale-group label="europe">
-      <locale lang="en"/>
-      <locale lang="es"/>
-      <locale lang="fr"/>
-      <locale lang="de"/>
+      <locale>en</locale>
+      <locale>es</locale>
+      <locale>fr</locale>
+      <locale>de</locale>
     </locale-group>
     <locale-group label="north-america">
-      <locale lang="en"/>
-      <locale lang="es" region="MX"/>
-      <locale lang="fr" region="CA"/>
-    </locale-group>
-    <locale-group label="all">
-      <locale/>
+      <locale>en</locale>
+      <locale>es-rMX</locale>
+      <locale>fr-rCA</locale>
     </locale-group>
     <android-sdk-group label="19">
       <android-sdk
@@ -156,10 +153,9 @@
   EXPECT_EQ(3ul, value.screen_density_groups["large"].size());
   EXPECT_EQ(6ul, value.screen_density_groups["alldpi"].size());
 
-  EXPECT_EQ(3ul, value.locale_groups.size());
+  EXPECT_EQ(2ul, value.locale_groups.size());
   EXPECT_EQ(4ul, value.locale_groups["europe"].size());
   EXPECT_EQ(3ul, value.locale_groups["north-america"].size());
-  EXPECT_EQ(1ul, value.locale_groups["all"].size());
 
   EXPECT_EQ(1ul, value.android_sdk_groups.size());
   EXPECT_EQ(1ul, value.android_sdk_groups["19"].size());
@@ -198,7 +194,7 @@
   EXPECT_EQ(1ul, config.artifacts.size());
 
   auto& artifact = config.artifacts.front();
-  EXPECT_EQ("", artifact.name); // TODO: make this fail.
+  EXPECT_FALSE(artifact.name);  // TODO: make this fail.
   EXPECT_EQ("arm", artifact.abi_group.value());
   EXPECT_EQ("large", artifact.screen_density_group.value());
   EXPECT_EQ("europe", artifact.locale_group.value());
@@ -298,10 +294,10 @@
 TEST_F(ConfigurationParserTest, LocaleGroupAction) {
   static constexpr const char* xml = R"xml(
     <locale-group label="europe">
-      <locale lang="en"/>
-      <locale lang="es"/>
-      <locale lang="fr"/>
-      <locale lang="de"/>
+      <locale>en</locale>
+      <locale>es</locale>
+      <locale>fr</locale>
+      <locale>de</locale>
     </locale-group>)xml";
 
   auto doc = test::BuildXmlDom(xml);
@@ -313,16 +309,12 @@
   ASSERT_EQ(1ul, config.locale_groups.size());
   ASSERT_EQ(1u, config.locale_groups.count("europe"));
 
-  auto& out = config.locale_groups["europe"];
+  const auto& out = config.locale_groups["europe"];
 
-  Locale en;
-  en.lang = std::string("en");
-  Locale es;
-  es.lang = std::string("es");
-  Locale fr;
-  fr.lang = std::string("fr");
-  Locale de;
-  de.lang = std::string("de");
+  ConfigDescription en = test::ParseConfigOrDie("en");
+  ConfigDescription es = test::ParseConfigOrDie("es");
+  ConfigDescription fr = test::ParseConfigOrDie("fr");
+  ConfigDescription de = test::ParseConfigOrDie("de");
 
   ASSERT_THAT(out, ElementsAre(en, es, fr, de));
 }
@@ -425,14 +417,14 @@
   Artifact x86;
   x86.abi_group = {"x86"};
 
-  auto x86_result = x86.ToArtifactName("something.{abi}.apk", &diag);
+  auto x86_result = x86.ToArtifactName("something.${abi}.apk", &diag);
   ASSERT_TRUE(x86_result);
   EXPECT_EQ(x86_result.value(), "something.x86.apk");
 
   Artifact arm;
   arm.abi_group = {"armeabi-v7a"};
 
-  auto arm_result = arm.ToArtifactName("app.{abi}.apk", &diag);
+  auto arm_result = arm.ToArtifactName("app.${abi}.apk", &diag);
   ASSERT_TRUE(arm_result);
   EXPECT_EQ(arm_result.value(), "app.armeabi-v7a.apk");
 }
@@ -447,8 +439,8 @@
   artifact.locale_group = {"en-AU"};
   artifact.android_sdk_group = {"26"};
 
-  auto result =
-      artifact.ToArtifactName("app.{density}_{locale}_{feature}_{gl}.sdk{sdk}.{abi}.apk", &diag);
+  auto result = artifact.ToArtifactName(
+      "app.${density}_${locale}_${feature}_${gl}.sdk${sdk}.${abi}.apk", &diag);
   ASSERT_TRUE(result);
   EXPECT_EQ(result.value(), "app.ldpi_en-AU_df1_glx1.sdk26.mips64.apk");
 }
@@ -458,7 +450,7 @@
   Artifact x86;
   x86.abi_group = {"x86"};
 
-  EXPECT_FALSE(x86.ToArtifactName("something.{density}.apk", &diag));
+  EXPECT_FALSE(x86.ToArtifactName("something.${density}.apk", &diag));
   EXPECT_FALSE(x86.ToArtifactName("something.apk", &diag));
 }
 
@@ -466,7 +458,7 @@
   StdErrDiagnostics diag;
   Artifact artifact;
 
-  EXPECT_FALSE(artifact.ToArtifactName("something.{density}.apk", &diag));
+  EXPECT_FALSE(artifact.ToArtifactName("something.${density}.apk", &diag));
   EXPECT_TRUE(artifact.ToArtifactName("something.apk", &diag));
 }
 
@@ -475,8 +467,8 @@
   Artifact artifact;
   artifact.screen_density_group = {"mdpi"};
 
-  EXPECT_TRUE(artifact.ToArtifactName("something.{density}.apk", &diag));
-  EXPECT_FALSE(artifact.ToArtifactName("something.{density}.{density}.apk", &diag));
+  ASSERT_TRUE(artifact.ToArtifactName("something.${density}.apk", &diag));
+  EXPECT_FALSE(artifact.ToArtifactName("something.${density}.${density}.apk", &diag));
 }
 
 TEST(ArtifactTest, Nesting) {
@@ -484,36 +476,36 @@
   Artifact x86;
   x86.abi_group = {"x86"};
 
-  EXPECT_FALSE(x86.ToArtifactName("something.{abi{density}}.apk", &diag));
+  EXPECT_FALSE(x86.ToArtifactName("something.${abi${density}}.apk", &diag));
 
-  const Maybe<std::string>& name = x86.ToArtifactName("something.{abi{abi}}.apk", &diag);
-  EXPECT_TRUE(name);
-  EXPECT_EQ(name.value(), "something.{abix86}.apk");
+  const Maybe<std::string>& name = x86.ToArtifactName("something.${abi${abi}}.apk", &diag);
+  ASSERT_TRUE(name);
+  EXPECT_EQ(name.value(), "something.${abix86}.apk");
 }
 
 TEST(ArtifactTest, Recursive) {
   StdErrDiagnostics diag;
   Artifact artifact;
-  artifact.device_feature_group = {"{gl}"};
+  artifact.device_feature_group = {"${gl}"};
   artifact.gl_texture_group = {"glx1"};
 
-  EXPECT_FALSE(artifact.ToArtifactName("app.{feature}.{gl}.apk", &diag));
+  EXPECT_FALSE(artifact.ToArtifactName("app.${feature}.${gl}.apk", &diag));
 
   artifact.device_feature_group = {"df1"};
-  artifact.gl_texture_group = {"{feature}"};
+  artifact.gl_texture_group = {"${feature}"};
   {
-    const auto& result = artifact.ToArtifactName("app.{feature}.{gl}.apk", &diag);
-    EXPECT_TRUE(result);
-    EXPECT_EQ(result.value(), "app.df1.{feature}.apk");
+    const auto& result = artifact.ToArtifactName("app.${feature}.${gl}.apk", &diag);
+    ASSERT_TRUE(result);
+    EXPECT_EQ(result.value(), "app.df1.${feature}.apk");
   }
 
   // This is an invalid case, but should be the only possible case due to the ordering of
   // replacement.
-  artifact.device_feature_group = {"{gl}"};
+  artifact.device_feature_group = {"${gl}"};
   artifact.gl_texture_group = {"glx1"};
   {
-    const auto& result = artifact.ToArtifactName("app.{feature}.apk", &diag);
-    EXPECT_TRUE(result);
+    const auto& result = artifact.ToArtifactName("app.${feature}.apk", &diag);
+    ASSERT_TRUE(result);
     EXPECT_EQ(result.value(), "app.glx1.apk");
   }
 }
diff --git a/tools/aapt2/filter/ConfigFilter.h b/tools/aapt2/filter/ConfigFilter.h
index 3f13416..ebb8151 100644
--- a/tools/aapt2/filter/ConfigFilter.h
+++ b/tools/aapt2/filter/ConfigFilter.h
@@ -38,13 +38,9 @@
 };
 
 /**
- * Implements config axis matching. An axis is one component of a configuration,
- * like screen
- * density or locale. If an axis is specified in the filter, and the axis is
- * specified in
- * the configuration to match, they must be compatible. Otherwise the
- * configuration to match is
- * accepted.
+ * Implements config axis matching. An axis is one component of a configuration, like screen density
+ * or locale. If an axis is specified in the filter, and the axis is specified in the configuration
+ * to match, they must be compatible. Otherwise the configuration to match is accepted.
  *
  * Used when handling "-c" options.
  */
diff --git a/tools/aapt2/filter/Filter_test.cpp b/tools/aapt2/filter/Filter_test.cpp
index fb75a4b..db2e69f 100644
--- a/tools/aapt2/filter/Filter_test.cpp
+++ b/tools/aapt2/filter/Filter_test.cpp
@@ -25,22 +25,16 @@
 namespace aapt {
 namespace {
 
-TEST(FilterChainTest, EmptyChain) {
+TEST(FilterTest, FilterChain) {
   FilterChain chain;
   ASSERT_TRUE(chain.Keep("some/random/path"));
-}
 
-TEST(FilterChainTest, SingleFilter) {
-  FilterChain chain;
   chain.AddFilter(util::make_unique<PrefixFilter>("keep/"));
 
   ASSERT_FALSE(chain.Keep("removed/path"));
   ASSERT_TRUE(chain.Keep("keep/path/1"));
   ASSERT_TRUE(chain.Keep("keep/path/2"));
-}
 
-TEST(FilterChainTest, MultipleFilters) {
-  FilterChain chain;
   chain.AddFilter(util::make_unique<PrefixFilter>("keep/"));
   chain.AddFilter(util::make_unique<PrefixFilter>("keep/really/"));
 
diff --git a/tools/aapt2/optimize/MultiApkGenerator.cpp b/tools/aapt2/optimize/MultiApkGenerator.cpp
new file mode 100644
index 0000000..f413ee9
--- /dev/null
+++ b/tools/aapt2/optimize/MultiApkGenerator.cpp
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2017 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 "MultiApkGenerator.h"
+
+#include <algorithm>
+#include <string>
+
+#include "androidfw/StringPiece.h"
+
+#include "LoadedApk.h"
+#include "configuration/ConfigurationParser.h"
+#include "filter/AbiFilter.h"
+#include "filter/Filter.h"
+#include "flatten/Archive.h"
+#include "process/IResourceTableConsumer.h"
+#include "split/TableSplitter.h"
+#include "util/Files.h"
+
+namespace aapt {
+
+using ::aapt::configuration::Artifact;
+using ::aapt::configuration::PostProcessingConfiguration;
+using ::android::StringPiece;
+
+MultiApkGenerator::MultiApkGenerator(LoadedApk* apk, IAaptContext* context)
+    : apk_(apk), context_(context) {
+}
+
+bool MultiApkGenerator::FromBaseApk(const std::string& out_dir,
+                                    const PostProcessingConfiguration& config,
+                                    const TableFlattenerOptions& table_flattener_options) {
+  // TODO(safarmer): Handle APK version codes for the generated APKs.
+  // TODO(safarmer): Handle explicit outputs/generating an output file list for other tools.
+
+  const std::string& apk_path = apk_->GetSource().path;
+  const StringPiece ext = file::GetExtension(apk_path);
+  const std::string base_name = apk_path.substr(0, apk_path.rfind(ext.to_string()));
+
+  // For now, just write out the stripped APK since ABI splitting doesn't modify anything else.
+  for (const Artifact& artifact : config.artifacts) {
+    FilterChain filters;
+    TableSplitterOptions splits;
+    AxisConfigFilter axis_filter;
+
+    if (!artifact.name && !config.artifact_format) {
+      context_->GetDiagnostics()->Error(
+          DiagMessage() << "Artifact does not have a name and no global name template defined");
+      return false;
+    }
+
+    Maybe<std::string> artifact_name =
+        (artifact.name)
+            ? artifact.Name(base_name, ext.substr(1), context_->GetDiagnostics())
+            : artifact.ToArtifactName(config.artifact_format.value(), context_->GetDiagnostics(),
+                                      base_name, ext.substr(1));
+
+    if (!artifact_name) {
+      context_->GetDiagnostics()->Error(DiagMessage()
+                                        << "Could not determine split APK artifact name");
+      return false;
+    }
+
+    if (artifact.abi_group) {
+      const std::string& group_name = artifact.abi_group.value();
+
+      auto group = config.abi_groups.find(group_name);
+      // TODO: Remove validation when configuration parser ensures referential integrity.
+      if (group == config.abi_groups.end()) {
+        context_->GetDiagnostics()->Error(DiagMessage() << "could not find referenced ABI group '"
+                                                        << group_name << "'");
+        return false;
+      }
+      filters.AddFilter(AbiFilter::FromAbiList(group->second));
+    }
+
+    if (artifact.screen_density_group) {
+      const std::string& group_name = artifact.screen_density_group.value();
+
+      auto group = config.screen_density_groups.find(group_name);
+      // TODO: Remove validation when configuration parser ensures referential integrity.
+      if (group == config.screen_density_groups.end()) {
+        context_->GetDiagnostics()->Error(DiagMessage() << "could not find referenced group '"
+                                                        << group_name << "'");
+        return false;
+      }
+
+      const std::vector<ConfigDescription>& densities = group->second;
+      std::for_each(densities.begin(), densities.end(), [&](const ConfigDescription& c) {
+        splits.preferred_densities.push_back(c.density);
+      });
+    }
+
+    if (artifact.locale_group) {
+      const std::string& group_name = artifact.locale_group.value();
+      auto group = config.locale_groups.find(group_name);
+      // TODO: Remove validation when configuration parser ensures referential integrity.
+      if (group == config.locale_groups.end()) {
+        context_->GetDiagnostics()->Error(DiagMessage() << "could not find referenced group '"
+                                                        << group_name << "'");
+        return false;
+      }
+
+      const std::vector<ConfigDescription>& locales = group->second;
+      std::for_each(locales.begin(), locales.end(),
+                    [&](const ConfigDescription& c) { axis_filter.AddConfig(c); });
+      splits.config_filter = &axis_filter;
+    }
+
+    std::unique_ptr<ResourceTable> table = apk_->GetResourceTable()->Clone();
+
+    TableSplitter splitter{{}, splits};
+    splitter.SplitTable(table.get());
+
+    std::string out = out_dir;
+    file::AppendPath(&out, artifact_name.value());
+
+    std::unique_ptr<IArchiveWriter> writer =
+        CreateZipFileArchiveWriter(context_->GetDiagnostics(), out);
+
+    if (context_->IsVerbose()) {
+      context_->GetDiagnostics()->Note(DiagMessage() << "Writing output: " << out);
+    }
+
+    if (!apk_->WriteToArchive(context_, table.get(), table_flattener_options, &filters,
+                              writer.get())) {
+      return false;
+    }
+  }
+
+  return true;
+}
+
+}  // namespace aapt
diff --git a/tools/aapt2/optimize/MultiApkGenerator.h b/tools/aapt2/optimize/MultiApkGenerator.h
new file mode 100644
index 0000000..f325d83
--- /dev/null
+++ b/tools/aapt2/optimize/MultiApkGenerator.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2017 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 AAPT2_APKSPLITTER_H
+#define AAPT2_APKSPLITTER_H
+
+#include "Diagnostics.h"
+#include "LoadedApk.h"
+#include "configuration/ConfigurationParser.h"
+
+namespace aapt {
+
+/**
+ * Generates a set of APKs that are a subset of the original base APKs. Each of the new APKs contain
+ * only the resources and assets for an artifact in the configuration file.
+ */
+class MultiApkGenerator {
+ public:
+  MultiApkGenerator(LoadedApk* apk, IAaptContext* context);
+
+  /**
+   * Writes a set of APKs to the provided output directory. Each APK is a subset fo the base APK and
+   * represents an artifact in the post processing configuration.
+   */
+  bool FromBaseApk(const std::string& out_dir,
+                   const configuration::PostProcessingConfiguration& config,
+                   const TableFlattenerOptions& table_flattener_options);
+
+ private:
+  IDiagnostics* GetDiagnostics() {
+    return context_->GetDiagnostics();
+  }
+
+  LoadedApk* apk_;
+  IAaptContext* context_;
+};
+
+}  // namespace aapt
+
+#endif  // AAPT2_APKSPLITTER_H
diff --git a/tools/aapt2/optimize/MultiApkGenerator_test.cpp b/tools/aapt2/optimize/MultiApkGenerator_test.cpp
new file mode 100644
index 0000000..6c928d9
--- /dev/null
+++ b/tools/aapt2/optimize/MultiApkGenerator_test.cpp
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2017 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 "optimize/MultiApkGenerator.h"
+
+#include <string>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+#include "LoadedApk.h"
+#include "ResourceTable.h"
+#include "configuration/ConfigurationParser.h"
+#include "filter/Filter.h"
+#include "flatten/Archive.h"
+#include "flatten/TableFlattener.h"
+#include "process/IResourceTableConsumer.h"
+#include "test/Context.h"
+#include "test/Test.h"
+
+namespace aapt {
+namespace {
+
+using ::aapt::configuration::Abi;
+using ::aapt::configuration::Artifact;
+using ::aapt::configuration::PostProcessingConfiguration;
+
+using ::testing::Eq;
+using ::testing::Return;
+using ::testing::_;
+
+class MockApk : public LoadedApk {
+ public:
+  MockApk(std::unique_ptr<ResourceTable> table) : LoadedApk({"test.apk"}, {}, std::move(table)){};
+  MOCK_METHOD5(WriteToArchive, bool(IAaptContext*, ResourceTable*, const TableFlattenerOptions&,
+                                    FilterChain*, IArchiveWriter*));
+};
+
+TEST(MultiApkGeneratorTest, FromBaseApk) {
+  std::unique_ptr<ResourceTable> table =
+      test::ResourceTableBuilder()
+          .AddFileReference("android:drawable/icon", "res/drawable-mdpi/icon.png",
+                            test::ParseConfigOrDie("mdpi"))
+          .AddFileReference("android:drawable/icon", "res/drawable-hdpi/icon.png",
+                            test::ParseConfigOrDie("hdpi"))
+          .AddFileReference("android:drawable/icon", "res/drawable-xhdpi/icon.png",
+                            test::ParseConfigOrDie("xhdpi"))
+          .AddFileReference("android:drawable/icon", "res/drawable-xxhdpi/icon.png",
+                            test::ParseConfigOrDie("xxhdpi"))
+          .AddSimple("android:string/one")
+          .Build();
+
+  MockApk apk{std::move(table)};
+
+  EXPECT_CALL(apk, WriteToArchive(_, _, _, _, _)).Times(0);
+
+  test::Context ctx;
+  PostProcessingConfiguration empty_config;
+  TableFlattenerOptions table_flattener_options;
+
+  MultiApkGenerator generator{&apk, &ctx};
+  EXPECT_TRUE(generator.FromBaseApk("out", empty_config, table_flattener_options));
+
+  Artifact x64 = test::ArtifactBuilder()
+                     .SetName("${basename}.x64.apk")
+                     .SetAbiGroup("x64")
+                     .SetLocaleGroup("en")
+                     .SetDensityGroup("xhdpi")
+                     .Build();
+
+  Artifact intel = test::ArtifactBuilder()
+                       .SetName("${basename}.intel.apk")
+                       .SetAbiGroup("intel")
+                       .SetLocaleGroup("europe")
+                       .SetDensityGroup("large")
+                       .Build();
+
+  auto config = test::PostProcessingConfigurationBuilder()
+                    .SetLocaleGroup("en", {"en"})
+                    .SetLocaleGroup("europe", {"en", "fr", "de", "es"})
+                    .SetAbiGroup("x64", {Abi::kX86_64})
+                    .SetAbiGroup("intel", {Abi::kX86_64, Abi::kX86})
+                    .SetDensityGroup("xhdpi", {"xhdpi"})
+                    .SetDensityGroup("large", {"xhdpi", "xxhdpi", "xxxhdpi"})
+                    .AddArtifact(x64)
+                    .AddArtifact(intel)
+                    .Build();
+
+  // Called once for each artifact.
+  EXPECT_CALL(apk, WriteToArchive(Eq(&ctx), _, _, _, _)).Times(2).WillRepeatedly(Return(true));
+  EXPECT_TRUE(generator.FromBaseApk("out", config, table_flattener_options));
+}
+
+}  // namespace
+}  // namespace aapt
diff --git a/tools/aapt2/split/TableSplitter.cpp b/tools/aapt2/split/TableSplitter.cpp
index 27e13d8..9d49ca6 100644
--- a/tools/aapt2/split/TableSplitter.cpp
+++ b/tools/aapt2/split/TableSplitter.cpp
@@ -32,8 +32,7 @@
 namespace aapt {
 
 using ConfigClaimedMap = std::unordered_map<ResourceConfigValue*, bool>;
-using ConfigDensityGroups =
-    std::map<ConfigDescription, std::vector<ResourceConfigValue*>>;
+using ConfigDensityGroups = std::map<ConfigDescription, std::vector<ResourceConfigValue*>>;
 
 static ConfigDescription CopyWithoutDensity(const ConfigDescription& config) {
   ConfigDescription without_density = config;
@@ -51,8 +50,7 @@
       if (config.density == 0) {
         density_independent_configs_.insert(config);
       } else {
-        density_dependent_config_to_density_map_[CopyWithoutDensity(config)] =
-            config.density;
+        density_dependent_config_to_density_map_[CopyWithoutDensity(config)] = config.density;
       }
     }
   }
@@ -94,9 +92,7 @@
 
         ResourceConfigValue* best_value = nullptr;
         for (ResourceConfigValue* this_value : related_values) {
-          if (!best_value ||
-              this_value->config.isBetterThan(best_value->config,
-                                              &target_density)) {
+          if (!best_value || this_value->config.isBetterThan(best_value->config, &target_density)) {
             best_value = this_value;
           }
         }
@@ -120,9 +116,8 @@
 };
 
 /**
- * Marking non-preferred densities as claimed will make sure the base doesn't
- * include them,
- * leaving only the preferred density behind.
+ * Marking non-preferred densities as claimed will make sure the base doesn't include them, leaving
+ * only the preferred density behind.
  */
 static void MarkNonPreferredDensitiesAsClaimed(
     const std::vector<uint16_t>& preferred_densities, const ConfigDensityGroups& density_groups,
@@ -161,8 +156,7 @@
   for (size_t i = 0; i < split_constraints_.size(); i++) {
     for (size_t j = i + 1; j < split_constraints_.size(); j++) {
       for (const ConfigDescription& config : split_constraints_[i].configs) {
-        if (split_constraints_[j].configs.find(config) !=
-            split_constraints_[j].configs.end()) {
+        if (split_constraints_[j].configs.find(config) != split_constraints_[j].configs.end()) {
           context->GetDiagnostics()->Error(DiagMessage()
                                            << "config '" << config
                                            << "' appears in multiple splits, "
@@ -193,28 +187,22 @@
       for (auto& entry : type->entries) {
         if (options_.config_filter) {
           // First eliminate any resource that we definitely don't want.
-          for (std::unique_ptr<ResourceConfigValue>& config_value :
-               entry->values) {
+          for (std::unique_ptr<ResourceConfigValue>& config_value : entry->values) {
             if (!options_.config_filter->Match(config_value->config)) {
-              // null out the entry. We will clean up and remove nulls at the
-              // end for performance reasons.
+              // null out the entry. We will clean up and remove nulls at the end for performance
+              // reasons.
               config_value.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.
+        // 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 density_groups;
         ConfigClaimedMap config_claimed_map;
-        for (const std::unique_ptr<ResourceConfigValue>& config_value :
-             entry->values) {
+        for (const std::unique_ptr<ResourceConfigValue>& config_value : entry->values) {
           if (config_value) {
             config_claimed_map[config_value.get()] = false;
 
@@ -226,9 +214,8 @@
           }
         }
 
-        // First we check all the splits. If it doesn't match one of the splits,
-        // we
-        // leave it in the base.
+        // 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 < split_count; idx++) {
           const SplitConstraints& split_constraint = split_constraints_[idx];
           ResourceTable* split_table = splits_[idx].get();
@@ -240,20 +227,16 @@
 
           // No need to do any work if we selected nothing.
           if (!selected_values.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* split_pkg =
-                split_table->FindPackage(pkg->name);
-            ResourceTableType* split_type =
-                split_pkg->FindOrCreateType(type->type);
+            // Create the same resource structure in the split. We do this lazily because we might
+            // not have actual values for each type/entry.
+            ResourceTablePackage* split_pkg = split_table->FindPackage(pkg->name);
+            ResourceTableType* split_type = split_pkg->FindOrCreateType(type->type);
             if (!split_type->id) {
               split_type->id = type->id;
               split_type->symbol_status = type->symbol_status;
             }
 
-            ResourceEntry* split_entry =
-                split_type->FindOrCreateEntry(entry->name);
+            ResourceEntry* split_entry = split_type->FindOrCreateEntry(entry->name);
             if (!split_entry->id) {
               split_entry->id = entry->id;
               split_entry->symbol_status = entry->symbol_status;
@@ -262,8 +245,7 @@
             // Copy the selected values into the new Split Entry.
             for (ResourceConfigValue* config_value : selected_values) {
               ResourceConfigValue* new_config_value =
-                  split_entry->FindOrCreateValue(config_value->config,
-                                                 config_value->product);
+                  split_entry->FindOrCreateValue(config_value->config, config_value->product);
               new_config_value->value = std::unique_ptr<Value>(
                   config_value->value->Clone(&split_table->string_pool));
             }
@@ -276,11 +258,9 @@
                                              &config_claimed_map);
         }
 
-        // All splits are handled, now check to see what wasn't claimed and
-        // remove
-        // whatever exists in other splits.
-        for (std::unique_ptr<ResourceConfigValue>& config_value :
-             entry->values) {
+        // All splits are handled, now check to see what wasn't claimed and remove whatever exists
+        // in other splits.
+        for (std::unique_ptr<ResourceConfigValue>& config_value : entry->values) {
           if (config_value && config_claimed_map[config_value.get()]) {
             // Claimed, remove from base.
             config_value.reset();
diff --git a/tools/aapt2/test/Builders.cpp b/tools/aapt2/test/Builders.cpp
index b579545..80e6adf 100644
--- a/tools/aapt2/test/Builders.cpp
+++ b/tools/aapt2/test/Builders.cpp
@@ -212,5 +212,63 @@
   return doc;
 }
 
+PostProcessingConfigurationBuilder& PostProcessingConfigurationBuilder::SetAbiGroup(
+    const std::string& name, const std::vector<configuration::Abi>& abis) {
+  config_.abi_groups[name] = abis;
+  return *this;
+}
+
+PostProcessingConfigurationBuilder& PostProcessingConfigurationBuilder::SetLocaleGroup(
+    const std::string& name, const std::vector<std::string>& locales) {
+  auto& group = config_.locale_groups[name];
+  for (const auto& locale : locales) {
+    group.push_back(ParseConfigOrDie(locale));
+  }
+  return *this;
+}
+
+PostProcessingConfigurationBuilder& PostProcessingConfigurationBuilder::SetDensityGroup(
+    const std::string& name, const std::vector<std::string>& densities) {
+  auto& group = config_.screen_density_groups[name];
+  for (const auto& density : densities) {
+    group.push_back(ParseConfigOrDie(density));
+  }
+  return *this;
+}
+
+PostProcessingConfigurationBuilder& PostProcessingConfigurationBuilder::AddArtifact(
+    const configuration::Artifact& artifact) {
+  config_.artifacts.push_back(artifact);
+  return *this;
+}
+
+configuration::PostProcessingConfiguration PostProcessingConfigurationBuilder::Build() {
+  return config_;
+}
+
+ArtifactBuilder& ArtifactBuilder::SetName(const std::string& name) {
+  artifact_.name = {name};
+  return *this;
+}
+
+ArtifactBuilder& ArtifactBuilder::SetAbiGroup(const std::string& name) {
+  artifact_.abi_group = {name};
+  return *this;
+}
+
+ArtifactBuilder& ArtifactBuilder::SetDensityGroup(const std::string& name) {
+  artifact_.screen_density_group = {name};
+  return *this;
+}
+
+ArtifactBuilder& ArtifactBuilder::SetLocaleGroup(const std::string& name) {
+  artifact_.locale_group = {name};
+  return *this;
+}
+
+configuration::Artifact ArtifactBuilder::Build() {
+  return artifact_;
+}
+
 }  // namespace test
 }  // namespace aapt
diff --git a/tools/aapt2/test/Builders.h b/tools/aapt2/test/Builders.h
index d9f3912..e8cefc1 100644
--- a/tools/aapt2/test/Builders.h
+++ b/tools/aapt2/test/Builders.h
@@ -24,7 +24,9 @@
 #include "Resource.h"
 #include "ResourceTable.h"
 #include "ResourceValues.h"
+#include "configuration/ConfigurationParser.h"
 #include "process/IResourceTableConsumer.h"
+#include "test/Common.h"
 #include "util/Maybe.h"
 #include "xml/XmlDom.h"
 
@@ -149,6 +151,37 @@
 std::unique_ptr<xml::XmlResource> BuildXmlDomForPackageName(IAaptContext* context,
                                                             const android::StringPiece& str);
 
+class PostProcessingConfigurationBuilder {
+ public:
+  PostProcessingConfigurationBuilder() = default;
+
+  PostProcessingConfigurationBuilder& SetAbiGroup(const std::string& name,
+                                                  const std::vector<configuration::Abi>& abis);
+  PostProcessingConfigurationBuilder& SetLocaleGroup(const std::string& name,
+                                                     const std::vector<std::string>& locales);
+  PostProcessingConfigurationBuilder& SetDensityGroup(const std::string& name,
+                                                      const std::vector<std::string>& densities);
+  PostProcessingConfigurationBuilder& AddArtifact(const configuration::Artifact& artifact);
+  configuration::PostProcessingConfiguration Build();
+
+ private:
+  configuration::PostProcessingConfiguration config_;
+};
+
+class ArtifactBuilder {
+ public:
+  ArtifactBuilder() = default;
+
+  ArtifactBuilder& SetName(const std::string& name);
+  ArtifactBuilder& SetAbiGroup(const std::string& name);
+  ArtifactBuilder& SetDensityGroup(const std::string& name);
+  ArtifactBuilder& SetLocaleGroup(const std::string& name);
+  configuration::Artifact Build();
+
+ private:
+  configuration::Artifact artifact_;
+};
+
 }  // namespace test
 }  // namespace aapt
 
diff --git a/tools/aapt2/test/Common.h b/tools/aapt2/test/Common.h
index d7b46ca..d53c92f 100644
--- a/tools/aapt2/test/Common.h
+++ b/tools/aapt2/test/Common.h
@@ -41,7 +41,7 @@
 
 inline ResourceName ParseNameOrDie(const android::StringPiece& str) {
   ResourceNameRef ref;
-  CHECK(ResourceUtils::ParseResourceName(str, &ref)) << "invalid resource name";
+  CHECK(ResourceUtils::ParseResourceName(str, &ref)) << "invalid resource name: " << str;
   return ref.ToResourceName();
 }