hiddenapi: Add 'list' command to generate public/private API lists

In order to make decisions about non-SDK API restrictions, we need
a complete list of public and private class members in the boot
class path. This was previously done by Doclava but having to lower
its Java view of the framework to dex level has proven cumbersome
and error prone.

This patch adds a new command to the `hiddenapi` build tool which
builds the class hierarchy of boot class path and attempts to resolve
each member of android.jar against it. Resolved members are dumped
into a public API text file, the rest into a private API text file.

Note that the resolution is not strictly the same as in ART and we
may err on the side of caution. This should be revisited to comply
with the spec.

Bug: 79409988
Test: m out/target/common/obj/PACKAGING/hiddenapi-private-list.txt
Change-Id: Ia82bcaad9347344aacf8dc6f7b093f769cd853ec
diff --git a/libdexfile/dex/dex_file.h b/libdexfile/dex/dex_file.h
index 714e42c..2eca8c7 100644
--- a/libdexfile/dex/dex_file.h
+++ b/libdexfile/dex/dex_file.h
@@ -1243,6 +1243,9 @@
   bool IsAtMethod() const {
     return pos_ >= EndOfInstanceFieldsPos();
   }
+  bool IsAtVirtualMethod() const {
+    return pos_ >= EndOfDirectMethodsPos();
+  }
   bool HasNextStaticField() const {
     return pos_ < EndOfStaticFieldsPos();
   }
diff --git a/tools/hiddenapi/hiddenapi.cc b/tools/hiddenapi/hiddenapi.cc
index 2e1ec5a..c252a9b 100644
--- a/tools/hiddenapi/hiddenapi.cc
+++ b/tools/hiddenapi/hiddenapi.cc
@@ -16,7 +16,8 @@
 
 #include <fstream>
 #include <iostream>
-#include <unordered_set>
+#include <map>
+#include <set>
 
 #include "android-base/stringprintf.h"
 #include "android-base/strings.h"
@@ -72,24 +73,71 @@
   UsageError("    --blacklist=<filename>:");
   UsageError("        text files with signatures of methods/fields to be annotated");
   UsageError("");
+  UsageError("  Command \"list\": dump lists of public and private API");
+  UsageError("    --boot-dex=<filename>: dex file which belongs to boot class path");
+  UsageError("    --stub-dex=<filename>: dex/apk file which belongs to SDK API stubs");
+  UsageError("");
+  UsageError("    --out-public=<filename>: output file for a list of all public APIs");
+  UsageError("    --out-private=<filename>: output file for a list of all private APIs");
+  UsageError("");
 
   exit(EXIT_FAILURE);
 }
 
+template<typename E>
+static bool Contains(const std::vector<E>& vec, const E& elem) {
+  return std::find(vec.begin(), vec.end(), elem) != vec.end();
+}
+
 class DexClass {
  public:
   DexClass(const DexFile& dex_file, uint32_t idx)
       : dex_file_(dex_file), class_def_(dex_file.GetClassDef(idx)) {}
 
   const DexFile& GetDexFile() const { return dex_file_; }
-
-  const dex::TypeIndex GetClassIndex() const { return class_def_.class_idx_; }
-
   const uint8_t* GetData() const { return dex_file_.GetClassData(class_def_); }
 
-  const char* GetDescriptor() const { return dex_file_.GetClassDescriptor(class_def_); }
+  const dex::TypeIndex GetClassIndex() const { return class_def_.class_idx_; }
+  const dex::TypeIndex GetSuperclassIndex() const { return class_def_.superclass_idx_; }
+
+  bool HasSuperclass() const { return dex_file_.IsTypeIndexValid(GetSuperclassIndex()); }
+
+  std::string GetDescriptor() const { return dex_file_.GetClassDescriptor(class_def_); }
+
+  std::string GetSuperclassDescriptor() const {
+    if (HasSuperclass()) {
+      return dex_file_.StringByTypeIdx(GetSuperclassIndex());
+    } else {
+      return "";
+    }
+  }
+
+  std::set<std::string> GetInterfaceDescriptors() const {
+    std::set<std::string> list;
+    const DexFile::TypeList* ifaces = dex_file_.GetInterfacesList(class_def_);
+    for (uint32_t i = 0; ifaces != nullptr && i < ifaces->Size(); ++i) {
+      list.insert(dex_file_.StringByTypeIdx(ifaces->GetTypeItem(i).type_idx_));
+    }
+    return list;
+  }
+
+  inline bool IsVisible() const { return HasAccessFlags(kAccPublic); }
+
+  inline bool Equals(const DexClass& other) const {
+    bool equals = GetDescriptor() == other.GetDescriptor();
+    if (equals) {
+      // TODO(dbrazdil): Check that methods/fields match as well once b/111116543 is fixed.
+      CHECK_EQ(GetAccessFlags(), other.GetAccessFlags());
+      CHECK_EQ(GetSuperclassDescriptor(), other.GetSuperclassDescriptor());
+      CHECK(GetInterfaceDescriptors() == other.GetInterfaceDescriptors());
+    }
+    return equals;
+  }
 
  private:
+  uint32_t GetAccessFlags() const { return class_def_.access_flags_; }
+  bool HasAccessFlags(uint32_t mask) const { return (GetAccessFlags() & mask) == mask; }
+
   const DexFile& dex_file_;
   const DexFile::ClassDef& class_def_;
 };
@@ -98,10 +146,12 @@
  public:
   DexMember(const DexClass& klass, const ClassDataItemIterator& it)
       : klass_(klass), it_(it) {
-    DCHECK_EQ(it_.IsAtMethod() ? GetMethodId().class_idx_ : GetFieldId().class_idx_,
+    DCHECK_EQ(IsMethod() ? GetMethodId().class_idx_ : GetFieldId().class_idx_,
               klass_.GetClassIndex());
   }
 
+  inline const DexClass& GetDeclaringClass() const { return klass_; }
+
   // Sets hidden bits in access flags and writes them back into the DEX in memory.
   // Note that this will not update the cached data of ClassDataItemIterator
   // until it iterates over this item again and therefore will fail a CHECK if
@@ -115,7 +165,7 @@
     // `ptr` initially points to the next ClassData item. We iterate backwards
     // until we hit the terminating byte of the previous Leb128 value.
     const uint8_t* ptr = it_.DataPointer();
-    if (it_.IsAtMethod()) {
+    if (IsMethod()) {
       ptr = ReverseSearchUnsignedLeb128(ptr);
       DCHECK_EQ(DecodeUnsignedLeb128WithoutMovingCursor(ptr), it_.GetMethodCodeItemOffset());
     }
@@ -126,30 +176,57 @@
     UpdateUnsignedLeb128(const_cast<uint8_t*>(ptr), new_flags);
   }
 
+  inline bool IsMethod() const { return it_.IsAtMethod(); }
+  inline bool IsVirtualMethod() const { return it_.IsAtVirtualMethod(); }
+
+  // Returns true if the member is public/protected and is in a public class.
+  inline bool IsVisible() const {
+    return GetDeclaringClass().IsVisible() &&
+           (HasAccessFlags(kAccPublic) || HasAccessFlags(kAccProtected));
+  }
+
   // Constructs a string with a unique signature of this class member.
   std::string GetApiEntry() const {
     std::stringstream ss;
-    ss << klass_.GetDescriptor() << "->";
-    if (it_.IsAtMethod()) {
-      const DexFile::MethodId& mid = GetMethodId();
-      ss << klass_.GetDexFile().GetMethodName(mid)
-         << klass_.GetDexFile().GetMethodSignature(mid).ToString();
-    } else {
-      const DexFile::FieldId& fid = GetFieldId();
-      ss << klass_.GetDexFile().GetFieldName(fid) << ":"
-         << klass_.GetDexFile().GetFieldTypeDescriptor(fid);
-    }
+    ss << klass_.GetDescriptor() << "->" << GetName() << (IsMethod() ? "" : ":") << GetSignature();
     return ss.str();
   }
 
+  inline bool operator==(const DexMember& other) {
+    // These need to match if they should resolve to one another.
+    bool equals = IsMethod() == other.IsMethod() &&
+                  GetName() == other.GetName() &&
+                  GetSignature() == other.GetSignature();
+
+    // Sanity checks if they do match.
+    if (equals) {
+      CHECK_EQ(IsVirtualMethod(), other.IsVirtualMethod());
+    }
+
+    return equals;
+  }
+
  private:
+  inline uint32_t GetAccessFlags() const { return it_.GetMemberAccessFlags(); }
+  inline uint32_t HasAccessFlags(uint32_t mask) const { return (GetAccessFlags() & mask) == mask; }
+
+  inline std::string GetName() const {
+    return IsMethod() ? klass_.GetDexFile().GetMethodName(GetMethodId())
+                      : klass_.GetDexFile().GetFieldName(GetFieldId());
+  }
+
+  inline std::string GetSignature() const {
+    return IsMethod() ? klass_.GetDexFile().GetMethodSignature(GetMethodId()).ToString()
+                      : klass_.GetDexFile().GetFieldTypeDescriptor(GetFieldId());
+  }
+
   inline const DexFile::MethodId& GetMethodId() const {
-    DCHECK(it_.IsAtMethod());
+    DCHECK(IsMethod());
     return klass_.GetDexFile().GetMethodId(it_.GetMemberIndex());
   }
 
   inline const DexFile::FieldId& GetFieldId() const {
-    DCHECK(!it_.IsAtMethod());
+    DCHECK(!IsMethod());
     return klass_.GetDexFile().GetFieldId(it_.GetMemberIndex());
   }
 
@@ -159,24 +236,31 @@
 
 class ClassPath FINAL {
  public:
-  explicit ClassPath(const std::vector<std::string>& dex_paths) {
-    OpenDexFiles(dex_paths);
+  ClassPath(const std::vector<std::string>& dex_paths, bool open_writable) {
+    OpenDexFiles(dex_paths, open_writable);
+  }
+
+  template<typename Fn>
+  void ForEachDexClass(Fn fn) {
+    for (auto& dex_file : dex_files_) {
+      for (uint32_t class_idx = 0; class_idx < dex_file->NumClassDefs(); ++class_idx) {
+        DexClass klass(*dex_file, class_idx);
+        fn(klass);
+      }
+    }
   }
 
   template<typename Fn>
   void ForEachDexMember(Fn fn) {
-    for (auto& dex_file : dex_files_) {
-      for (uint32_t class_idx = 0; class_idx < dex_file->NumClassDefs(); ++class_idx) {
-        DexClass klass(*dex_file, class_idx);
-        const uint8_t* klass_data = klass.GetData();
-        if (klass_data != nullptr) {
-          for (ClassDataItemIterator it(*dex_file, klass_data); it.HasNext(); it.Next()) {
-            DexMember member(klass, it);
-            fn(member);
-          }
+    ForEachDexClass([&fn](DexClass& klass) {
+      const uint8_t* klass_data = klass.GetData();
+      if (klass_data != nullptr) {
+        for (ClassDataItemIterator it(klass.GetDexFile(), klass_data); it.HasNext(); it.Next()) {
+          DexMember member(klass, it);
+          fn(member);
         }
       }
-    }
+    });
   }
 
   void UpdateDexChecksums() {
@@ -189,37 +273,231 @@
   }
 
  private:
-  void OpenDexFiles(const std::vector<std::string>& dex_paths) {
+  void OpenDexFiles(const std::vector<std::string>& dex_paths, bool open_writable) {
     ArtDexFileLoader dex_loader;
     std::string error_msg;
-    for (const std::string& filename : dex_paths) {
-      File fd(filename.c_str(), O_RDWR, /* check_usage */ false);
-      CHECK_NE(fd.Fd(), -1) << "Unable to open file '" << filename << "': " << strerror(errno);
 
-      // Memory-map the dex file with MAP_SHARED flag so that changes in memory
-      // propagate to the underlying file. We run dex file verification as if
-      // the dex file was not in boot claass path to check basic assumptions,
-      // such as that at most one of public/private/protected flag is set.
-      // We do those checks here and skip them when loading the processed file
-      // into boot class path.
-      std::unique_ptr<const DexFile> dex_file(dex_loader.OpenDex(fd.Release(),
-                                                                 /* location */ filename,
-                                                                 /* verify */ true,
-                                                                 /* verify_checksum */ true,
-                                                                 /* mmap_shared */ true,
-                                                                 &error_msg));
-      CHECK(dex_file.get() != nullptr) << "Open failed for '" << filename << "' " << error_msg;
-      CHECK(dex_file->IsStandardDexFile()) << "Expected a standard dex file '" << filename << "'";
-      CHECK(dex_file->EnableWrite())
-          << "Failed to enable write permission for '" << filename << "'";
-      dex_files_.push_back(std::move(dex_file));
+    if (open_writable) {
+      for (const std::string& filename : dex_paths) {
+        File fd(filename.c_str(), O_RDWR, /* check_usage */ false);
+        CHECK_NE(fd.Fd(), -1) << "Unable to open file '" << filename << "': " << strerror(errno);
+
+        // Memory-map the dex file with MAP_SHARED flag so that changes in memory
+        // propagate to the underlying file. We run dex file verification as if
+        // the dex file was not in boot claass path to check basic assumptions,
+        // such as that at most one of public/private/protected flag is set.
+        // We do those checks here and skip them when loading the processed file
+        // into boot class path.
+        std::unique_ptr<const DexFile> dex_file(dex_loader.OpenDex(fd.Release(),
+                                                                   /* location */ filename,
+                                                                   /* verify */ true,
+                                                                   /* verify_checksum */ true,
+                                                                   /* mmap_shared */ true,
+                                                                   &error_msg));
+        CHECK(dex_file.get() != nullptr) << "Open failed for '" << filename << "' " << error_msg;
+        CHECK(dex_file->IsStandardDexFile()) << "Expected a standard dex file '" << filename << "'";
+        CHECK(dex_file->EnableWrite())
+            << "Failed to enable write permission for '" << filename << "'";
+        dex_files_.push_back(std::move(dex_file));
+      }
+    } else {
+      for (const std::string& filename : dex_paths) {
+        bool success = dex_loader.Open(filename.c_str(),
+                                       /* location */ filename,
+                                       /* verify */ true,
+                                       /* verify_checksum */ true,
+                                       &error_msg,
+                                       &dex_files_);
+        CHECK(success) << "Open failed for '" << filename << "' " << error_msg;
+      }
     }
   }
 
-  // Opened DEX files. Note that these are opened as `const` but may be written into.
+  // Opened dex files. Note that these are opened as `const` but may be written into.
   std::vector<std::unique_ptr<const DexFile>> dex_files_;
 };
 
+class HierarchyClass FINAL {
+ public:
+  HierarchyClass() {}
+
+  void AddDexClass(const DexClass& klass) {
+    CHECK(dex_classes_.empty() || klass.Equals(dex_classes_.front()));
+    dex_classes_.push_back(klass);
+  }
+
+  void AddExtends(HierarchyClass& parent) {
+    CHECK(!Contains(extends_, &parent));
+    CHECK(!Contains(parent.extended_by_, this));
+    extends_.push_back(&parent);
+    parent.extended_by_.push_back(this);
+  }
+
+  const DexClass& GetOneDexClass() const {
+    CHECK(!dex_classes_.empty());
+    return dex_classes_.front();
+  }
+
+  // See comment on Hierarchy::ForEachResolvableMember.
+  template<typename Fn>
+  bool ForEachResolvableMember(const DexMember& other, Fn fn) {
+    return ForEachResolvableMember_Impl(other, fn) != ResolutionResult::kNotFound;
+  }
+
+ private:
+  // Result of resolution which takes into account whether the member was found
+  // for the first time or not. This is just a performance optimization to prevent
+  // re-visiting previously visited members.
+  // Note that order matters. When accumulating results, we always pick the maximum.
+  enum class ResolutionResult {
+    kNotFound,
+    kFoundOld,
+    kFoundNew,
+  };
+
+  inline ResolutionResult Accumulate(ResolutionResult a, ResolutionResult b) {
+    return static_cast<ResolutionResult>(
+        std::max(static_cast<unsigned>(a), static_cast<unsigned>(b)));
+  }
+
+  template<typename Fn>
+  ResolutionResult ForEachResolvableMember_Impl(const DexMember& other, Fn fn) {
+    // First try to find a member matching `other` in this class.
+    ResolutionResult foundInClass = ForEachMatchingMember(other, fn);
+
+    switch (foundInClass) {
+      case ResolutionResult::kFoundOld:
+        // A matching member was found and previously explored. All subclasses
+        // must have been explored too.
+        break;
+
+      case ResolutionResult::kFoundNew:
+        // A matching member was found and this was the first time it was visited.
+        // If it is a virtual method, visit all methods overriding/implementing it too.
+        if (other.IsVirtualMethod()) {
+          for (HierarchyClass* subclass : extended_by_) {
+            subclass->ForEachOverridingMember(other, fn);
+          }
+        }
+        break;
+
+      case ResolutionResult::kNotFound:
+        // A matching member was not found in this class. Explore the superclasses
+        // and implemented interfaces.
+        for (HierarchyClass* superclass : extends_) {
+          foundInClass = Accumulate(
+              foundInClass, superclass->ForEachResolvableMember_Impl(other, fn));
+        }
+        break;
+    }
+
+    return foundInClass;
+  }
+
+  template<typename Fn>
+  ResolutionResult ForEachMatchingMember(const DexMember& other, Fn fn) {
+    ResolutionResult found = ResolutionResult::kNotFound;
+    for (const DexClass& dex_class : dex_classes_) {
+      const uint8_t* data = dex_class.GetData();
+      if (data != nullptr) {
+        for (ClassDataItemIterator it(dex_class.GetDexFile(), data); it.HasNext(); it.Next()) {
+          DexMember member(dex_class, it);
+          if (member == other) {
+            found = Accumulate(found, fn(member) ? ResolutionResult::kFoundNew
+                                                 : ResolutionResult::kFoundOld);
+          }
+        }
+      }
+    }
+    return found;
+  }
+
+  template<typename Fn>
+  void ForEachOverridingMember(const DexMember& other, Fn fn) {
+    CHECK(other.IsVirtualMethod());
+    ResolutionResult found = ForEachMatchingMember(other, fn);
+    if (found == ResolutionResult::kFoundOld) {
+      // No need to explore further.
+      return;
+    } else {
+      for (HierarchyClass* subclass : extended_by_) {
+        subclass->ForEachOverridingMember(other, fn);
+      }
+    }
+  }
+
+  // DexClass entries of this class found across all the provided dex files.
+  std::vector<DexClass> dex_classes_;
+
+  // Classes which this class inherits, or interfaces which it implements.
+  std::vector<HierarchyClass*> extends_;
+
+  // Classes which inherit from this class.
+  std::vector<HierarchyClass*> extended_by_;
+};
+
+class Hierarchy FINAL {
+ public:
+  explicit Hierarchy(ClassPath& class_path) : class_path_(class_path) {
+    BuildClassHierarchy();
+  }
+
+  // Perform an operation for each member of the hierarchy which could potentially
+  // be the result of method/field resolution of `other`.
+  // The function `fn` should accept a DexMember reference and return true if
+  // the member was changed. This drives a performance optimization which only
+  // visits overriding members the first time the overridden member is visited.
+  // Returns true if at least one resolvable member was found.
+  template<typename Fn>
+  bool ForEachResolvableMember(const DexMember& other, Fn fn) {
+    HierarchyClass* klass = FindClass(other.GetDeclaringClass().GetDescriptor());
+    return (klass != nullptr) && klass->ForEachResolvableMember(other, fn);
+  }
+
+ private:
+  HierarchyClass* FindClass(const std::string& descriptor) {
+    auto it = classes_.find(descriptor);
+    if (it == classes_.end()) {
+      return nullptr;
+    } else {
+      return &it->second;
+    }
+  }
+
+  void BuildClassHierarchy() {
+    // Create one HierarchyClass entry in `classes_` per class descriptor
+    // and add all DexClass objects with the same descriptor to that entry.
+    class_path_.ForEachDexClass([this](DexClass& klass) {
+      classes_[klass.GetDescriptor()].AddDexClass(klass);
+    });
+
+    // Connect each HierarchyClass to its successors and predecessors.
+    for (auto& entry : classes_) {
+      HierarchyClass& klass = entry.second;
+      const DexClass& dex_klass = klass.GetOneDexClass();
+
+      if (!dex_klass.HasSuperclass()) {
+        CHECK(dex_klass.GetInterfaceDescriptors().empty())
+            << "java/lang/Object should not implement any interfaces";
+        continue;
+      }
+
+      HierarchyClass* superclass = FindClass(dex_klass.GetSuperclassDescriptor());
+      CHECK(superclass != nullptr);
+      klass.AddExtends(*superclass);
+
+      for (const std::string& iface_desc : dex_klass.GetInterfaceDescriptors()) {
+        HierarchyClass* iface = FindClass(iface_desc);
+        CHECK(iface != nullptr);
+        klass.AddExtends(*iface);
+      }
+    }
+  }
+
+  ClassPath& class_path_;
+  std::map<std::string, HierarchyClass> classes_;
+};
+
 class HiddenApi FINAL {
  public:
   HiddenApi() {}
@@ -229,14 +507,16 @@
     case Command::kEncode:
       EncodeAccessFlags();
       break;
+    case Command::kList:
+      ListApi();
+      break;
     }
   }
 
  private:
   enum class Command {
-    // Currently just one command. A "list" command will be added for generating
-    // a full list of boot class members.
     kEncode,
+    kList,
   };
 
   Command ParseArgs(int argc, char** argv) {
@@ -262,6 +542,22 @@
           }
         }
         return Command::kEncode;
+      } else if (command == "list") {
+        for (int i = 1; i < argc; ++i) {
+          const StringPiece option(argv[i]);
+          if (option.starts_with("--boot-dex=")) {
+            boot_dex_paths_.push_back(option.substr(strlen("--boot-dex=")).ToString());
+          } else if (option.starts_with("--stub-dex=")) {
+            stub_dex_paths_.push_back(option.substr(strlen("--stub-dex=")).ToString());
+          } else if (option.starts_with("--out-public=")) {
+            out_public_path_ = option.substr(strlen("--out-public=")).ToString();
+          } else if (option.starts_with("--out-private=")) {
+            out_private_path_ = option.substr(strlen("--out-private=")).ToString();
+          } else {
+            Usage("Unknown argument '%s'", option.data());
+          }
+        }
+        return Command::kList;
       } else {
         Usage("Unknown command '%s'", command.data());
       }
@@ -282,7 +578,7 @@
     OpenApiFile(blacklist_path_, api_list, HiddenApiAccessFlags::kBlacklist);
 
     // Open all dex files.
-    ClassPath boot_class_path(boot_dex_paths_);
+    ClassPath boot_class_path(boot_dex_paths_, /* open_writable */ true);
 
     // Set access flags of all members.
     boot_class_path.ForEachDexMember([&api_list](DexMember& boot_member) {
@@ -315,13 +611,90 @@
     api_file.close();
   }
 
+  void ListApi() {
+    if (boot_dex_paths_.empty()) {
+      Usage("No boot DEX files specified");
+    } else if (stub_dex_paths_.empty()) {
+      Usage("No stub DEX files specified");
+    } else if (out_public_path_.empty()) {
+      Usage("No public API output path specified");
+    } else if (out_private_path_.empty()) {
+      Usage("No private API output path specified");
+    }
+
+    // Complete list of boot class path members. The associated boolean states
+    // whether it is public (true) or private (false).
+    std::map<std::string, bool> boot_members;
+
+    // Deduplicate errors before printing them.
+    std::set<std::string> unresolved;
+
+    // Open all dex files.
+    ClassPath stub_class_path(stub_dex_paths_, /* open_writable */ false);
+    ClassPath boot_class_path(boot_dex_paths_, /* open_writable */ false);
+    Hierarchy boot_hierarchy(boot_class_path);
+
+    // Mark all boot dex members private.
+    boot_class_path.ForEachDexMember([&boot_members](DexMember& boot_member) {
+      boot_members[boot_member.GetApiEntry()] = false;
+    });
+
+    // Resolve each SDK dex member against the framework and mark it white.
+    stub_class_path.ForEachDexMember(
+        [&boot_hierarchy, &boot_members, &unresolved](DexMember& stub_member) {
+          if (!stub_member.IsVisible()) {
+            // Typically fake constructors and inner-class `this` fields.
+            return;
+          }
+          bool resolved = boot_hierarchy.ForEachResolvableMember(
+              stub_member,
+              [&boot_members](DexMember& boot_member) {
+                std::string entry = boot_member.GetApiEntry();
+                auto it = boot_members.find(entry);
+                CHECK(it != boot_members.end());
+                if (it->second) {
+                  return false;  // has been marked before
+                } else {
+                  boot_members.insert(it, std::make_pair(entry, true));
+                  return true;  // marked for the first time
+                }
+              });
+          if (!resolved) {
+            unresolved.insert(stub_member.GetApiEntry());
+          }
+        });
+
+    // Print errors.
+    for (const std::string& str : unresolved) {
+      LOG(WARNING) << "unresolved: " << str;
+    }
+
+    // Write into public/private API files.
+    std::ofstream file_public(out_public_path_.c_str());
+    std::ofstream file_private(out_private_path_.c_str());
+    for (const std::pair<std::string, bool> entry : boot_members) {
+      if (entry.second) {
+        file_public << entry.first << std::endl;
+      } else {
+        file_private << entry.first << std::endl;
+      }
+    }
+    file_public.close();
+    file_private.close();
+  }
+
   // Paths to DEX files which should be processed.
   std::vector<std::string> boot_dex_paths_;
+  std::vector<std::string> stub_dex_paths_;
 
   // Paths to text files which contain the lists of API members.
   std::string light_greylist_path_;
   std::string dark_greylist_path_;
   std::string blacklist_path_;
+
+  // Paths to text files to which we will output list of all API members.
+  std::string out_public_path_;
+  std::string out_private_path_;
 };
 
 }  // namespace art