AAPT2: Split APK by ABI.

Added a FilterChain that can apply multiple filter steps to an APK file
as it is being written to disk. The first filter applied is by ABI. If
a library in the APK does not match the filter it is skipped.

Added an AbiFilter that keeps files that are either not native libs or
are for the set of wanted ABIs

Test: ran unit tests locally
Test: ran against an APK with ARM and x68 libs and diffed the results

Change-Id: I3fb901d3de3513e85f2a2763a8e4487a28ed4881
diff --git a/tools/aapt2/Android.bp b/tools/aapt2/Android.bp
index 1dc47f8..d39a8a8 100644
--- a/tools/aapt2/Android.bp
+++ b/tools/aapt2/Android.bp
@@ -85,6 +85,7 @@
         "compile/Pseudolocalizer.cpp",
         "compile/XmlIdCollector.cpp",
         "configuration/ConfigurationParser.cpp",
+        "filter/AbiFilter.cpp",
         "filter/ConfigFilter.cpp",
         "flatten/Archive.cpp",
         "flatten/TableFlattener.cpp",
diff --git a/tools/aapt2/LoadedApk.cpp b/tools/aapt2/LoadedApk.cpp
index 8a8f8be..7e5efa1 100644
--- a/tools/aapt2/LoadedApk.cpp
+++ b/tools/aapt2/LoadedApk.cpp
@@ -57,6 +57,12 @@
 
 bool LoadedApk::WriteToArchive(IAaptContext* context, const TableFlattenerOptions& options,
                                IArchiveWriter* writer) {
+  FilterChain empty;
+  return WriteToArchive(context, options, &empty, writer);
+}
+
+bool LoadedApk::WriteToArchive(IAaptContext* context, 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) {
@@ -89,6 +95,13 @@
       continue;
     }
 
+    if (!filters->Keep(path)) {
+      if (context->IsVerbose()) {
+        context->GetDiagnostics()->Note(DiagMessage() << "Filtered '" << path << "' from APK.");
+      }
+      continue;
+    }
+
     // The resource table needs to be re-serialized since it might have changed.
     if (path == "resources.arsc") {
       BigBuffer buffer(4096);
diff --git a/tools/aapt2/LoadedApk.h b/tools/aapt2/LoadedApk.h
index 59eb816..8aa9674 100644
--- a/tools/aapt2/LoadedApk.h
+++ b/tools/aapt2/LoadedApk.h
@@ -20,6 +20,7 @@
 #include "androidfw/StringPiece.h"
 
 #include "ResourceTable.h"
+#include "filter/Filter.h"
 #include "flatten/Archive.h"
 #include "flatten/TableFlattener.h"
 #include "io/ZipArchive.h"
@@ -49,6 +50,14 @@
   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);
+
   static std::unique_ptr<LoadedApk> LoadApkFromPath(IAaptContext* context,
                                                     const android::StringPiece& path);
 
diff --git a/tools/aapt2/cmd/Optimize.cpp b/tools/aapt2/cmd/Optimize.cpp
index 194c0c8..0d69c89 100644
--- a/tools/aapt2/cmd/Optimize.cpp
+++ b/tools/aapt2/cmd/Optimize.cpp
@@ -17,6 +17,7 @@
 #include <memory>
 #include <vector>
 
+#include "android-base/stringprintf.h"
 #include "androidfw/StringPiece.h"
 
 #include "Diagnostics.h"
@@ -26,6 +27,8 @@
 #include "SdkConstants.h"
 #include "ValueVisitor.h"
 #include "cmd/Util.h"
+#include "configuration/ConfigurationParser.h"
+#include "filter/AbiFilter.h"
 #include "flatten/TableFlattener.h"
 #include "flatten/XmlFlattener.h"
 #include "io/BigBufferInputStream.h"
@@ -33,14 +36,21 @@
 #include "optimize/ResourceDeduper.h"
 #include "optimize/VersionCollapser.h"
 #include "split/TableSplitter.h"
+#include "util/Files.h"
 
-using android::StringPiece;
+using ::aapt::configuration::Abi;
+using ::aapt::configuration::Artifact;
+using ::aapt::configuration::Configuration;
+using ::android::StringPiece;
+using ::android::base::StringPrintf;
 
 namespace aapt {
 
 struct OptimizeOptions {
   // Path to the output APK.
-  std::string output_path;
+  Maybe<std::string> output_path;
+  // Path to the output APK directory for splits.
+  Maybe<std::string> output_dir;
 
   // Details of the app extracted from the AndroidManifest.xml
   AppInfo app_info;
@@ -55,6 +65,9 @@
   std::vector<SplitConstraints> split_constraints;
 
   TableFlattenerOptions table_flattener_options;
+
+  // TODO: Come up with a better name for the Configuration struct.
+  Maybe<Configuration> configuration;
 };
 
 class OptimizeContext : public IAaptContext {
@@ -175,10 +188,52 @@
       ++split_constraints_iter;
     }
 
-    std::unique_ptr<IArchiveWriter> writer =
-        CreateZipFileArchiveWriter(context_->GetDiagnostics(), options_.output_path);
-    if (!apk->WriteToArchive(context_, options_.table_flattener_options, writer.get())) {
-      return 1;
+    if (options_.configuration && options_.output_dir) {
+      Configuration& 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;
+          }
+        }
+      }
+    }
+
+    if (options_.output_path) {
+      std::unique_ptr<IArchiveWriter> writer =
+          CreateZipFileArchiveWriter(context_->GetDiagnostics(), options_.output_path.value());
+      if (!apk->WriteToArchive(context_, options_.table_flattener_options, writer.get())) {
+        return 1;
+      }
     }
 
     return 0;
@@ -214,8 +269,8 @@
             if (file_ref->file == nullptr) {
               ResourceNameRef name(pkg->name, type->type, entry->name);
               context_->GetDiagnostics()->Warn(DiagMessage(file_ref->GetSource())
-                                                << "file for resource " << name << " with config '"
-                                                << config_value->config << "' not found");
+                                               << "file for resource " << name << " with config '"
+                                               << config_value->config << "' not found");
               continue;
             }
 
@@ -293,13 +348,16 @@
 int Optimize(const std::vector<StringPiece>& args) {
   OptimizeContext context;
   OptimizeOptions options;
+  Maybe<std::string> config_path;
   Maybe<std::string> target_densities;
   std::vector<std::string> configs;
   std::vector<std::string> split_args;
   bool verbose = false;
   Flags flags =
       Flags()
-          .RequiredFlag("-o", "Path to the output APK.", &options.output_path)
+          .OptionalFlag("-o", "Path to the output APK.", &options.output_path)
+          .OptionalFlag("-d", "Path to the output directory (for splits).", &options.output_dir)
+          .OptionalFlag("-x", "Path to XML configuration file.", &config_path)
           .OptionalFlag(
               "--target-densities",
               "Comma separated list of the screen densities that the APK will be optimized for.\n"
@@ -369,6 +427,22 @@
     }
   }
 
+  if (config_path) {
+    if (!options.output_dir) {
+      context.GetDiagnostics()->Error(
+          DiagMessage() << "Output directory is required when using a configuration file");
+      return 1;
+    }
+    std::string& path = config_path.value();
+    Maybe<ConfigurationParser> for_path = ConfigurationParser::ForPath(path);
+    if (for_path) {
+      options.configuration = for_path.value().WithDiagnostics(context.GetDiagnostics()).Parse();
+    } else {
+      context.GetDiagnostics()->Error(DiagMessage() << "Could not parse config file " << path);
+      return 1;
+    }
+  }
+
   if (!ExtractAppDataFromManifest(&context, apk.get(), &options)) {
     return 1;
   }
diff --git a/tools/aapt2/configuration/ConfigurationParser.cpp b/tools/aapt2/configuration/ConfigurationParser.cpp
index 303a809..555cb35 100644
--- a/tools/aapt2/configuration/ConfigurationParser.cpp
+++ b/tools/aapt2/configuration/ConfigurationParser.cpp
@@ -18,6 +18,7 @@
 
 #include <algorithm>
 #include <functional>
+#include <map>
 #include <memory>
 #include <utility>
 
@@ -56,15 +57,15 @@
 using ::aapt::xml::XmlNodeAction;
 using ::android::base::ReadFileToString;
 
-const std::unordered_map<std::string, Abi> kAbiMap = {
-    {"armeabi", Abi::kArmeV6},
-    {"armeabi-v7a", Abi::kArmV7a},
-    {"arm64-v8a", Abi::kArm64V8a},
-    {"x86", Abi::kX86},
-    {"x86_64", Abi::kX86_64},
-    {"mips", Abi::kMips},
-    {"mips64", Abi::kMips64},
-    {"universal", Abi::kUniversal},
+const std::unordered_map<std::string, Abi> kStringToAbiMap = {
+    {"armeabi", Abi::kArmeV6}, {"armeabi-v7a", Abi::kArmV7a},  {"arm64-v8a", Abi::kArm64V8a},
+    {"x86", Abi::kX86},        {"x86_64", Abi::kX86_64},       {"mips", Abi::kMips},
+    {"mips64", Abi::kMips64},  {"universal", Abi::kUniversal},
+};
+const std::map<Abi, std::string> kAbiToStringMap = {
+    {Abi::kArmeV6, "armeabi"}, {Abi::kArmV7a, "armeabi-v7a"},  {Abi::kArm64V8a, "arm64-v8a"},
+    {Abi::kX86, "x86"},        {Abi::kX86_64, "x86_64"},       {Abi::kMips, "mips"},
+    {Abi::kMips64, "mips64"},  {Abi::kUniversal, "universal"},
 };
 
 constexpr const char* kAaptXmlNs = "http://schemas.android.com/tools/aapt";
@@ -102,7 +103,13 @@
 
 }  // namespace
 
+namespace configuration {
 
+const std::string& AbiToString(Abi abi) {
+  return kAbiToStringMap.find(abi)->second;
+}
+
+}  // namespace configuration
 
 /** Returns a ConfigurationParser for the file located at the provided path. */
 Maybe<ConfigurationParser> ConfigurationParser::ForPath(const std::string& path) {
@@ -175,6 +182,9 @@
     return {};
   }
 
+  // TODO: Validate all references in the configuration are valid. It should be safe to assume from
+  // this point on that any references from one section to another will be present.
+
   return {config};
 }
 
@@ -201,7 +211,7 @@
               DiagMessage() << "Unknown artifact attribute: " << attr.name << " = " << attr.value);
         }
       }
-      config->artifacts[artifact.name] = artifact;
+      config->artifacts.push_back(artifact);
       return true;
     };
 
@@ -236,7 +246,7 @@
           for (auto& node : child->children) {
             xml::Text* t;
             if ((t = NodeCast<xml::Text>(node.get())) != nullptr) {
-              group.push_back(kAbiMap.at(TrimWhitespace(t->text).to_string()));
+              group.push_back(kStringToAbiMap.at(TrimWhitespace(t->text).to_string()));
               break;
             }
           }
diff --git a/tools/aapt2/configuration/ConfigurationParser.h b/tools/aapt2/configuration/ConfigurationParser.h
index 8b9c085..0435cbf 100644
--- a/tools/aapt2/configuration/ConfigurationParser.h
+++ b/tools/aapt2/configuration/ConfigurationParser.h
@@ -62,6 +62,9 @@
   kUniversal
 };
 
+/** Helper method to convert an ABI to a string representing the path within the APK. */
+const std::string& AbiToString(Abi abi);
+
 /**
  * Represents an individual locale. When a locale is included, it must be
  * declared from least specific to most specific, as a region does not make
@@ -118,7 +121,8 @@
  * AAPT2 XML configuration binary representation.
  */
 struct Configuration {
-  std::unordered_map<std::string, Artifact> artifacts;
+  // TODO: Support named artifacts?
+  std::vector<Artifact> artifacts;
   Maybe<std::string> artifact_format;
 
   Group<Abi> abi_groups;
diff --git a/tools/aapt2/configuration/ConfigurationParser_test.cpp b/tools/aapt2/configuration/ConfigurationParser_test.cpp
index 8421ee3..a8c3858 100644
--- a/tools/aapt2/configuration/ConfigurationParser_test.cpp
+++ b/tools/aapt2/configuration/ConfigurationParser_test.cpp
@@ -196,7 +196,7 @@
 
   EXPECT_EQ(1ul, config.artifacts.size());
 
-  auto& artifact = config.artifacts.begin()->second;
+  auto& artifact = config.artifacts.front();
   EXPECT_EQ("", artifact.name); // TODO: make this fail.
   EXPECT_EQ("arm", artifact.abi_group.value());
   EXPECT_EQ("large", artifact.screen_density_group.value());
@@ -204,6 +204,21 @@
   EXPECT_EQ("19", artifact.android_sdk_group.value());
   EXPECT_EQ("dxt1", artifact.gl_texture_group.value());
   EXPECT_EQ("low-latency", artifact.device_feature_group.value());
+
+  // Perform a second action to ensure we get 2 artifacts.
+  static constexpr const char* second = R"xml(
+    <artifact
+        abi-group="other"
+        screen-density-group="large"
+        locale-group="europe"
+        android-sdk-group="19"
+        gl-texture-group="dxt1"
+        device-feature-group="low-latency"/>)xml";
+  doc = test::BuildXmlDom(second);
+
+  ok = artifact_handler_(&config, NodeCast<Element>(doc.get()->root.get()), &diag_);
+  ASSERT_TRUE(ok);
+  EXPECT_EQ(2ul, config.artifacts.size());
 }
 
 TEST_F(ConfigurationParserTest, ArtifactFormatAction) {
diff --git a/tools/aapt2/filter/AbiFilter.cpp b/tools/aapt2/filter/AbiFilter.cpp
new file mode 100644
index 0000000..cb96235
--- /dev/null
+++ b/tools/aapt2/filter/AbiFilter.cpp
@@ -0,0 +1,51 @@
+/*
+ * 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 "AbiFilter.h"
+
+#include <memory>
+
+#include "io/Util.h"
+
+namespace aapt {
+
+std::unique_ptr<AbiFilter> AbiFilter::FromAbiList(const std::vector<configuration::Abi>& abi_list) {
+  std::unordered_set<std::string> abi_set;
+  for (auto& abi : abi_list) {
+    abi_set.insert(configuration::AbiToString(abi));
+  }
+  // Make unique by hand as the constructor is private.
+  return std::unique_ptr<AbiFilter>(new AbiFilter(abi_set));
+}
+
+bool AbiFilter::Keep(const std::string& path) {
+  // We only care about libraries.
+  if (!util::StartsWith(path, kLibPrefix)) {
+    return true;
+  }
+
+  auto abi_end = path.find('/', kLibPrefixLen);
+  if (abi_end == std::string::npos) {
+    // Ignore any files in the top level lib directory.
+    return true;
+  }
+
+  // Strip the lib/ prefix.
+  const std::string& path_abi = path.substr(kLibPrefixLen, abi_end - kLibPrefixLen);
+  return (abis_.find(path_abi) != abis_.end());
+}
+
+}  // namespace aapt
diff --git a/tools/aapt2/filter/AbiFilter.h b/tools/aapt2/filter/AbiFilter.h
new file mode 100644
index 0000000..d875cb2
--- /dev/null
+++ b/tools/aapt2/filter/AbiFilter.h
@@ -0,0 +1,54 @@
+/*
+ * 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_ABISPLITTER_H
+#define AAPT2_ABISPLITTER_H
+
+#include <memory>
+#include <string>
+#include <unordered_set>
+#include <vector>
+
+#include "configuration/ConfigurationParser.h"
+#include "filter/Filter.h"
+
+namespace aapt {
+
+/**
+ * Filters native library paths by ABI. ABIs present in the filter list are kept and all over
+ * libraries are removed. The filter is only applied to native library paths (this under lib/).
+ */
+class AbiFilter : public IPathFilter {
+ public:
+  /** Factory method to create a filter from a list of configuration::Abi. */
+  static std::unique_ptr<AbiFilter> FromAbiList(const std::vector<configuration::Abi>& abi_list);
+
+  /** Returns true if the path is for a native library in the list of desired ABIs. */
+  bool Keep(const std::string& path) override;
+
+ private:
+  explicit AbiFilter(std::unordered_set<std::string> abis) : abis_(std::move(abis)) {
+  }
+
+  /** The path prefix to where all native libs end up inside an APK file. */
+  static constexpr const char* kLibPrefix = "lib/";
+  static constexpr size_t kLibPrefixLen = 4;
+  const std::unordered_set<std::string> abis_;
+};
+
+}  // namespace aapt
+
+#endif  // AAPT2_ABISPLITTER_H
diff --git a/tools/aapt2/filter/AbiFilter_test.cpp b/tools/aapt2/filter/AbiFilter_test.cpp
new file mode 100644
index 0000000..0c8ea35
--- /dev/null
+++ b/tools/aapt2/filter/AbiFilter_test.cpp
@@ -0,0 +1,66 @@
+/*
+ * 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 "filter/AbiFilter.h"
+
+#include <string>
+
+#include "gtest/gtest.h"
+
+namespace aapt {
+namespace {
+
+using ::aapt::configuration::Abi;
+
+struct TestData {
+  std::string path;
+  bool kept;
+};
+
+const TestData kTestData[] = {
+    /* Keep. */
+    {"lib/mips/libnative.so", true},
+    {"not/native/file.txt", true},
+    // Not sure if this is a valid use case.
+    {"lib/listing.txt", true},
+    {"lib/mips/foo/bar/baz.so", true},
+    {"lib/mips/x86/foo.so", true},
+    /* Discard. */
+    {"lib/mips_horse/foo.so", false},
+    {"lib/horse_mips/foo.so", false},
+    {"lib/mips64/armeabi-v7a/foo.so", false},
+    {"lib/mips64/x86_64/x86.so", false},
+    {"lib/x86/libnative.so", false},
+    {"lib/x86/foo/bar/baz.so", false},
+    {"lib/x86/x86/foo.so", false},
+    {"lib/x86_horse/foo.so", false},
+    {"lib/horse_x86/foo.so", false},
+    {"lib/x86/armeabi-v7a/foo.so", false},
+    {"lib/x86_64/x86_64/x86.so", false},
+};
+
+class AbiFilterTest : public ::testing::TestWithParam<TestData> {};
+
+TEST_P(AbiFilterTest, Keep) {
+  auto mips = AbiFilter::FromAbiList({Abi::kMips});
+  const TestData& data = GetParam();
+  EXPECT_EQ(mips->Keep(data.path), data.kept);
+}
+
+INSTANTIATE_TEST_CASE_P(NativePaths, AbiFilterTest, ::testing::ValuesIn(kTestData));
+
+}  // namespace
+}  // namespace aapt
diff --git a/tools/aapt2/filter/Filter.h b/tools/aapt2/filter/Filter.h
new file mode 100644
index 0000000..d737dc9
--- /dev/null
+++ b/tools/aapt2/filter/Filter.h
@@ -0,0 +1,77 @@
+/*
+ * 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_FILTER_H
+#define AAPT2_FILTER_H
+
+#include <string>
+#include <vector>
+
+#include "util/Util.h"
+
+namespace aapt {
+
+/** A filter to be applied to a path segment. */
+class IPathFilter {
+ public:
+  ~IPathFilter() = default;
+
+  /** Returns true if the path should be kept. */
+  virtual bool Keep(const std::string& path) = 0;
+};
+
+/**
+ * Path filter that keeps anything that matches the provided prefix.
+ */
+class PrefixFilter : public IPathFilter {
+ public:
+  explicit PrefixFilter(std::string prefix) : prefix_(std::move(prefix)) {
+  }
+
+  /** Returns true if the provided path matches the prefix. */
+  bool Keep(const std::string& path) override {
+    return util::StartsWith(path, prefix_);
+  }
+
+ private:
+  const std::string prefix_;
+};
+
+/** Applies a set of IPathFilters to a path and returns true iif all filters keep the path. */
+class FilterChain : public IPathFilter {
+ public:
+  /** Adds a filter to the list to be applied to each path. */
+  void AddFilter(std::unique_ptr<IPathFilter> filter) {
+    filters_.push_back(std::move(filter));
+  }
+
+  /** Returns true if all filters keep the path. */
+  bool Keep(const std::string& path) override {
+    for (auto& filter : filters_) {
+      if (!filter->Keep(path)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+ private:
+  std::vector<std::unique_ptr<IPathFilter>> filters_;
+};
+
+}  // namespace aapt
+
+#endif  // AAPT2_FILTER_H
diff --git a/tools/aapt2/filter/Filter_test.cpp b/tools/aapt2/filter/Filter_test.cpp
new file mode 100644
index 0000000..fb75a4b
--- /dev/null
+++ b/tools/aapt2/filter/Filter_test.cpp
@@ -0,0 +1,54 @@
+/*
+ * 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 "filter/Filter.h"
+
+#include <string>
+
+#include "io/Util.h"
+
+#include "gtest/gtest.h"
+
+namespace aapt {
+namespace {
+
+TEST(FilterChainTest, EmptyChain) {
+  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/"));
+
+  ASSERT_FALSE(chain.Keep("removed/path"));
+  ASSERT_FALSE(chain.Keep("/keep/really/wrong/prefix"));
+  ASSERT_FALSE(chain.Keep("keep/maybe/1"));
+  ASSERT_TRUE(chain.Keep("keep/really/1"));
+}
+
+}  // namespace
+}  // namespace aapt