AAPT2: Better debugging output

Test: make aapt2_tests
Change-Id: I7778b773201381538dc1f2e376abee4eb33e44c0
diff --git a/tools/aapt2/Android.bp b/tools/aapt2/Android.bp
index ae67f61..eb3a99a 100644
--- a/tools/aapt2/Android.bp
+++ b/tools/aapt2/Android.bp
@@ -113,6 +113,7 @@
         "optimize/VersionCollapser.cpp",
         "process/SymbolTable.cpp",
         "split/TableSplitter.cpp",
+        "text/Printer.cpp",
         "text/Unicode.cpp",
         "text/Utf8Iterator.cpp",
         "util/BigBuffer.cpp",
diff --git a/tools/aapt2/ConfigDescription.cpp b/tools/aapt2/ConfigDescription.cpp
index 59a6e12..f621660 100644
--- a/tools/aapt2/ConfigDescription.cpp
+++ b/tools/aapt2/ConfigDescription.cpp
@@ -882,6 +882,11 @@
   return std::string(locale);
 }
 
+std::string ConfigDescription::to_string() const {
+  const android::String8 str = toString();
+  return std::string(str.string(), str.size());
+}
+
 bool ConfigDescription::Dominates(const ConfigDescription& o) const {
   if (*this == o) {
     return true;
diff --git a/tools/aapt2/ConfigDescription.h b/tools/aapt2/ConfigDescription.h
index c1d0e10..f719552 100644
--- a/tools/aapt2/ConfigDescription.h
+++ b/tools/aapt2/ConfigDescription.h
@@ -64,6 +64,8 @@
   // Returns the BCP-47 language tag of this configuration's locale.
   std::string GetBcp47LanguageTag(bool canonicalize = false) const;
 
+  std::string to_string() const;
+
   /**
    * A configuration X dominates another configuration Y, if X has at least the
    * precedence of Y and X is strictly more general than Y: for any type defined
diff --git a/tools/aapt2/Debug.cpp b/tools/aapt2/Debug.cpp
index 61c304b..08efc27 100644
--- a/tools/aapt2/Debug.cpp
+++ b/tools/aapt2/Debug.cpp
@@ -17,7 +17,6 @@
 #include "Debug.h"
 
 #include <algorithm>
-#include <iostream>
 #include <map>
 #include <memory>
 #include <queue>
@@ -25,120 +24,244 @@
 #include <vector>
 
 #include "android-base/logging.h"
+#include "android-base/stringprintf.h"
 
 #include "ResourceTable.h"
 #include "ResourceValues.h"
 #include "ValueVisitor.h"
+#include "text/Printer.h"
 #include "util/Util.h"
 
+using ::aapt::text::Printer;
+using ::android::StringPiece;
+using ::android::base::StringPrintf;
+
 namespace aapt {
 
 namespace {
 
-class PrintVisitor : public ConstValueVisitor {
+class ValueHeadlinePrinter : public ConstValueVisitor {
  public:
   using ConstValueVisitor::Visit;
 
+  explicit ValueHeadlinePrinter(const std::string& package, Printer* printer)
+      : package_(package), printer_(printer) {
+  }
+
   void Visit(const Attribute* attr) override {
-    std::cout << "(attr) type=";
-    attr->PrintMask(&std::cout);
-    static constexpr uint32_t kMask =
-        android::ResTable_map::TYPE_ENUM | android::ResTable_map::TYPE_FLAGS;
+    printer_->Print("(attr) type=");
+    printer_->Print(attr->MaskString());
+    if (!attr->symbols.empty()) {
+      printer_->Print(StringPrintf(" size=%zd", attr->symbols.size()));
+    }
+  }
+
+  void Visit(const Style* style) override {
+    printer_->Print(StringPrintf("(style) size=%zd", style->entries.size()));
+    if (style->parent) {
+      printer_->Print(" parent=");
+
+      const Reference& parent_ref = style->parent.value();
+      if (parent_ref.name) {
+        if (parent_ref.private_reference) {
+          printer_->Print("*");
+        }
+
+        const ResourceName& parent_name = parent_ref.name.value();
+        if (package_ != parent_name.package) {
+          printer_->Print(parent_name.package);
+          printer_->Print(":");
+        }
+        printer_->Print(to_string(parent_name.type));
+        printer_->Print("/");
+        printer_->Print(parent_name.entry);
+        if (parent_ref.id) {
+          printer_->Print(" (");
+          printer_->Print(parent_ref.id.value().to_string());
+          printer_->Print(")");
+        }
+      } else if (parent_ref.id) {
+        printer_->Print(parent_ref.id.value().to_string());
+      } else {
+        printer_->Print("???");
+      }
+    }
+  }
+
+  void Visit(const Array* array) override {
+    printer_->Print(StringPrintf("(array) size=%zd", array->elements.size()));
+  }
+
+  void Visit(const Plural* plural) override {
+    size_t count = std::count_if(plural->values.begin(), plural->values.end(),
+                                 [](const std::unique_ptr<Item>& v) { return v != nullptr; });
+    printer_->Print(StringPrintf("(plurals) size=%zd", count));
+  }
+
+  void Visit(const Styleable* styleable) override {
+    printer_->Println(StringPrintf("(styleable) size=%zd", styleable->entries.size()));
+  }
+
+  void VisitItem(const Item* item) override {
+    // Pretty much guaranteed to be one line.
+    if (const Reference* ref = ValueCast<Reference>(item)) {
+      // Special case Reference so that we can print local resources without a package name.
+      ref->PrettyPrint(package_, printer_);
+    } else {
+      item->PrettyPrint(printer_);
+    }
+  }
+
+ private:
+  std::string package_;
+  Printer* printer_;
+};
+
+class ValueBodyPrinter : public ConstValueVisitor {
+ public:
+  using ConstValueVisitor::Visit;
+
+  explicit ValueBodyPrinter(const std::string& package, Printer* printer)
+      : package_(package), printer_(printer) {
+  }
+
+  void Visit(const Attribute* attr) override {
+    constexpr uint32_t kMask = android::ResTable_map::TYPE_ENUM | android::ResTable_map::TYPE_FLAGS;
     if (attr->type_mask & kMask) {
       for (const auto& symbol : attr->symbols) {
-        std::cout << "\n        " << symbol.symbol.name.value().entry;
+        printer_->Print(symbol.symbol.name.value().entry);
         if (symbol.symbol.id) {
-          std::cout << " (" << symbol.symbol.id.value() << ")";
+          printer_->Print("(");
+          printer_->Print(symbol.symbol.id.value().to_string());
+          printer_->Print(")");
         }
-        std::cout << " = " << symbol.value;
+        printer_->Println(StringPrintf("=0x%08x", symbol.value));
       }
     }
   }
 
   void Visit(const Style* style) override {
-    std::cout << "(style)";
-    if (style->parent) {
-      const Reference& parent_ref = style->parent.value();
-      std::cout << " parent=";
-      if (parent_ref.name) {
-        if (parent_ref.private_reference) {
-          std::cout << "*";
-        }
-        std::cout << parent_ref.name.value() << " ";
-      }
-
-      if (parent_ref.id) {
-        std::cout << parent_ref.id.value();
-      }
-    }
-
     for (const auto& entry : style->entries) {
-      std::cout << "\n        ";
       if (entry.key.name) {
         const ResourceName& name = entry.key.name.value();
-        if (!name.package.empty()) {
-          std::cout << name.package << ":";
+        if (!name.package.empty() && name.package != package_) {
+          printer_->Print(name.package);
+          printer_->Print(":");
         }
-        std::cout << name.entry;
+        printer_->Print(name.entry);
+
+        if (entry.key.id) {
+          printer_->Print("(");
+          printer_->Print(entry.key.id.value().to_string());
+          printer_->Print(")");
+        }
+      } else if (entry.key.id) {
+        printer_->Print(entry.key.id.value().to_string());
+      } else {
+        printer_->Print("???");
       }
 
-      if (entry.key.id) {
-        std::cout << "(" << entry.key.id.value() << ")";
-      }
-
-      std::cout << "=" << *entry.value;
+      printer_->Print("=");
+      PrintItem(*entry.value);
+      printer_->Println();
     }
   }
 
   void Visit(const Array* array) override {
-    array->Print(&std::cout);
+    const size_t count = array->elements.size();
+    printer_->Print("[");
+    if (count > 0) {
+      for (size_t i = 0u; i < count; i++) {
+        if (i != 0u && i % 4u == 0u) {
+          printer_->Println();
+          printer_->Print(" ");
+        }
+        PrintItem(*array->elements[i]);
+        if (i != count - 1) {
+          printer_->Print(", ");
+        }
+      }
+      printer_->Println("]");
+    }
   }
 
   void Visit(const Plural* plural) override {
-    plural->Print(&std::cout);
+    constexpr std::array<const char*, Plural::Count> kPluralNames = {
+        {"zero", "one", "two", "few", "many", "other"}};
+
+    for (size_t i = 0; i < Plural::Count; i++) {
+      if (plural->values[i] != nullptr) {
+        printer_->Print(StringPrintf("%s=", kPluralNames[i]));
+        PrintItem(*plural->values[i]);
+        printer_->Println();
+      }
+    }
   }
 
   void Visit(const Styleable* styleable) override {
-    std::cout << "(styleable)";
     for (const auto& attr : styleable->entries) {
-      std::cout << "\n        ";
       if (attr.name) {
         const ResourceName& name = attr.name.value();
-        if (!name.package.empty()) {
-          std::cout << name.package << ":";
+        if (!name.package.empty() && name.package != package_) {
+          printer_->Print(name.package);
+          printer_->Print(":");
         }
-        std::cout << name.entry;
+        printer_->Print(name.entry);
+
+        if (attr.id) {
+          printer_->Print("(");
+          printer_->Print(attr.id.value().to_string());
+          printer_->Print(")");
+        }
       }
 
       if (attr.id) {
-        std::cout << "(" << attr.id.value() << ")";
+        printer_->Print(attr.id.value().to_string());
       }
     }
   }
 
   void VisitItem(const Item* item) override {
-    item->Print(&std::cout);
+    // Intentionally left empty, we already printed the Items.
   }
+
+ private:
+  void PrintItem(const Item& item) {
+    if (const Reference* ref = ValueCast<Reference>(&item)) {
+      // Special case Reference so that we can print local resources without a package name.
+      ref->PrettyPrint(package_, printer_);
+    } else {
+      item.PrettyPrint(printer_);
+    }
+  }
+
+  std::string package_;
+  Printer* printer_;
 };
 
 }  // namespace
 
-void Debug::PrintTable(const ResourceTable& table, const DebugPrintTableOptions& options) {
-  PrintVisitor visitor;
-
+void Debug::PrintTable(const ResourceTable& table, const DebugPrintTableOptions& options,
+                       Printer* printer) {
   for (const auto& package : table.packages) {
-    std::cout << "Package name=" << package->name;
-    if (package->id) {
-      std::cout << " id=" << std::hex << (int)package->id.value() << std::dec;
-    }
-    std::cout << std::endl;
+    ValueHeadlinePrinter headline_printer(package->name, printer);
+    ValueBodyPrinter body_printer(package->name, printer);
 
+    printer->Print("Package name=");
+    printer->Print(package->name);
+    if (package->id) {
+      printer->Print(StringPrintf(" id=%02x", package->id.value()));
+    }
+    printer->Println();
+
+    printer->Indent();
     for (const auto& type : package->types) {
-      std::cout << "\n  type " << type->type;
+      printer->Print("type ");
+      printer->Print(to_string(type->type));
       if (type->id) {
-        std::cout << " id=" << std::hex << (int)type->id.value() << std::dec;
+        printer->Print(StringPrintf(" id=%02x", type->id.value()));
       }
-      std::cout << " entryCount=" << type->entries.size() << std::endl;
+      printer->Println(StringPrintf(" entryCount=%zd", type->entries.size()));
 
       std::vector<const ResourceEntry*> sorted_entries;
       for (const auto& entry : type->entries) {
@@ -156,35 +279,54 @@
         sorted_entries.insert(iter, entry.get());
       }
 
+      printer->Indent();
       for (const ResourceEntry* entry : sorted_entries) {
         const ResourceId id(package->id.value_or_default(0), type->id.value_or_default(0),
                             entry->id.value_or_default(0));
-        const ResourceName name(package->name, type->type, entry->name);
 
-        std::cout << "    spec resource " << id << " " << name;
+        printer->Print("resource ");
+        printer->Print(id.to_string());
+        printer->Print(" ");
+
+        // Write the name without the package (this is obvious and too verbose).
+        printer->Print(to_string(type->type));
+        printer->Print("/");
+        printer->Print(entry->name);
+
         switch (entry->symbol_status.state) {
           case SymbolState::kPublic:
-            std::cout << " PUBLIC";
+            printer->Print(" PUBLIC");
             break;
           case SymbolState::kPrivate:
-            std::cout << " _PRIVATE_";
+            printer->Print(" _PRIVATE_");
             break;
-          default:
+          case SymbolState::kUndefined:
+            // Print nothing.
             break;
         }
 
-        std::cout << std::endl;
+        printer->Println();
 
+        printer->Indent();
         for (const auto& value : entry->values) {
-          std::cout << "      (" << value->config << ") ";
-          value->value->Accept(&visitor);
+          printer->Print("(");
+          printer->Print(value->config.to_string());
+          printer->Print(") ");
+          value->value->Accept(&headline_printer);
           if (options.show_sources && !value->value->GetSource().path.empty()) {
-            std::cout << " src=" << value->value->GetSource();
+            printer->Print(" src=");
+            printer->Print(value->value->GetSource().to_string());
           }
-          std::cout << std::endl;
+          printer->Println();
+          printer->Indent();
+          value->value->Accept(&body_printer);
+          printer->Undent();
         }
+        printer->Undent();
       }
+      printer->Undent();
     }
+    printer->Undent();
   }
 }
 
diff --git a/tools/aapt2/Debug.h b/tools/aapt2/Debug.h
index 296d04b..3c1ee4c 100644
--- a/tools/aapt2/Debug.h
+++ b/tools/aapt2/Debug.h
@@ -22,6 +22,7 @@
 
 #include "Resource.h"
 #include "ResourceTable.h"
+#include "text/Printer.h"
 #include "xml/XmlDom.h"
 
 namespace aapt {
@@ -31,9 +32,9 @@
 };
 
 struct Debug {
-  static void PrintTable(const ResourceTable& table, const DebugPrintTableOptions& options = {});
-  static void PrintStyleGraph(ResourceTable* table,
-                              const ResourceName& target_style);
+  static void PrintTable(const ResourceTable& table, const DebugPrintTableOptions& options,
+                         text::Printer* printer);
+  static void PrintStyleGraph(ResourceTable* table, const ResourceName& target_style);
   static void DumpHex(const void* data, size_t len);
   static void DumpXml(const xml::XmlResource& doc);
 };
diff --git a/tools/aapt2/Resource.cpp b/tools/aapt2/Resource.cpp
index a9f5f29..b78f48c 100644
--- a/tools/aapt2/Resource.cpp
+++ b/tools/aapt2/Resource.cpp
@@ -17,13 +17,34 @@
 #include "Resource.h"
 
 #include <map>
+#include <sstream>
 #include <string>
 
-using android::StringPiece;
+#include "android-base/stringprintf.h"
+
+using ::android::StringPiece;
+using ::android::base::StringPrintf;
 
 namespace aapt {
 
-StringPiece ToString(ResourceType type) {
+std::string ResourceId::to_string() const {
+  return StringPrintf("0x%08x", id);
+}
+
+std::string ResourceName::to_string() const {
+  return ResourceNameRef(*this).to_string();
+}
+
+std::string ResourceNameRef::to_string() const {
+  std::ostringstream str_stream;
+  if (!package.empty()) {
+    str_stream << package << ":";
+  }
+  str_stream << type << "/" << entry;
+  return str_stream.str();
+}
+
+StringPiece to_string(ResourceType type) {
   switch (type) {
     case ResourceType::kAnim:
       return "anim";
diff --git a/tools/aapt2/Resource.h b/tools/aapt2/Resource.h
index 87b9867..96a0203 100644
--- a/tools/aapt2/Resource.h
+++ b/tools/aapt2/Resource.h
@@ -69,11 +69,10 @@
   kXml,
 };
 
-android::StringPiece ToString(ResourceType type);
+android::StringPiece to_string(ResourceType type);
 
 /**
- * Returns a pointer to a valid ResourceType, or nullptr if
- * the string was invalid.
+ * Returns a pointer to a valid ResourceType, or nullptr if the string was invalid.
  */
 const ResourceType* ParseResourceType(const android::StringPiece& str);
 
@@ -92,7 +91,7 @@
   int compare(const ResourceName& other) const;
 
   bool is_valid() const;
-  std::string ToString() const;
+  std::string to_string() const;
 };
 
 /**
@@ -115,8 +114,10 @@
   ResourceNameRef& operator=(ResourceNameRef&& rhs) = default;
   ResourceNameRef& operator=(const ResourceName& rhs);
 
-  ResourceName ToResourceName() const;
   bool is_valid() const;
+
+  ResourceName ToResourceName() const;
+  std::string to_string() const;
 };
 
 constexpr const uint8_t kAppPackageId = 0x7fu;
@@ -149,6 +150,8 @@
   uint8_t package_id() const;
   uint8_t type_id() const;
   uint16_t entry_id() const;
+
+  std::string to_string() const;
 };
 
 struct SourcedResourceName {
@@ -229,7 +232,9 @@
   return (id & 0xff000000u) != 0 && (id & 0x00ff0000u) != 0;
 }
 
-inline bool ResourceId::is_valid_dynamic() const { return (id & 0x00ff0000u) != 0; }
+inline bool ResourceId::is_valid_dynamic() const {
+  return (id & 0x00ff0000u) != 0;
+}
 
 inline uint8_t ResourceId::package_id() const {
   return static_cast<uint8_t>(id >> 24);
@@ -259,24 +264,16 @@
   return lhs.id != rhs.id;
 }
 
-inline ::std::ostream& operator<<(::std::ostream& out,
-                                  const ResourceId& res_id) {
-  std::ios_base::fmtflags old_flags = out.flags();
-  char old_fill = out.fill();
-  out << "0x" << std::internal << std::setfill('0') << std::setw(8) << std::hex
-      << res_id.id;
-  out.flags(old_flags);
-  out.fill(old_fill);
-  return out;
+inline ::std::ostream& operator<<(::std::ostream& out, const ResourceId& res_id) {
+  return out << res_id.to_string();
 }
 
 //
 // ResourceType implementation.
 //
 
-inline ::std::ostream& operator<<(::std::ostream& out,
-                                  const ResourceType& val) {
-  return out << ToString(val);
+inline ::std::ostream& operator<<(::std::ostream& out, const ResourceType& val) {
+  return out << to_string(val);
 }
 
 //
@@ -315,18 +312,8 @@
          std::tie(rhs.package, rhs.type, rhs.entry);
 }
 
-inline ::std::ostream& operator<<(::std::ostream& out,
-                                  const ResourceName& name) {
-  if (!name.package.empty()) {
-    out << name.package << ":";
-  }
-  return out << name.type << "/" << name.entry;
-}
-
-inline std::string ResourceName::ToString() const {
-  std::stringstream stream;
-  stream << *this;
-  return stream.str();
+inline ::std::ostream& operator<<(::std::ostream& out, const ResourceName& name) {
+  return out << name.to_string();
 }
 
 //
@@ -370,12 +357,8 @@
          std::tie(rhs.package, rhs.type, rhs.entry);
 }
 
-inline ::std::ostream& operator<<(::std::ostream& out,
-                                  const ResourceNameRef& name) {
-  if (!name.package.empty()) {
-    out << name.package << ":";
-  }
-  return out << name.type << "/" << name.entry;
+inline ::std::ostream& operator<<(::std::ostream& out, const ResourceNameRef& name) {
+  return out << name.to_string();
 }
 
 inline bool operator<(const ResourceName& lhs, const ResourceNameRef& b) {
@@ -386,8 +369,7 @@
   return ResourceNameRef(lhs) != rhs;
 }
 
-inline bool operator==(const SourcedResourceName& lhs,
-                       const SourcedResourceName& rhs) {
+inline bool operator==(const SourcedResourceName& lhs, const SourcedResourceName& rhs) {
   return lhs.name == rhs.name && lhs.line == rhs.line;
 }
 
diff --git a/tools/aapt2/ResourceValues.cpp b/tools/aapt2/ResourceValues.cpp
index 082fd86..b38d259 100644
--- a/tools/aapt2/ResourceValues.cpp
+++ b/tools/aapt2/ResourceValues.cpp
@@ -17,9 +17,12 @@
 #include "ResourceValues.h"
 
 #include <algorithm>
+#include <cinttypes>
 #include <limits>
 #include <set>
+#include <sstream>
 
+#include "android-base/stringprintf.h"
 #include "androidfw/ResourceTypes.h"
 
 #include "Resource.h"
@@ -27,8 +30,18 @@
 #include "ValueVisitor.h"
 #include "util/Util.h"
 
+using ::aapt::text::Printer;
+using ::android::StringPiece;
+using ::android::base::StringPrintf;
+
 namespace aapt {
 
+void Value::PrettyPrint(Printer* printer) const {
+  std::ostringstream str_stream;
+  Print(&str_stream);
+  printer->Print(str_stream.str());
+}
+
 std::ostream& operator<<(std::ostream& out, const Value& value) {
   value.Print(&out);
   return out;
@@ -155,6 +168,49 @@
   }
 }
 
+static void PrettyPrintReferenceImpl(const Reference& ref, bool print_package, Printer* printer) {
+  switch (ref.reference_type) {
+    case Reference::Type::kResource:
+      printer->Print("@");
+      break;
+
+    case Reference::Type::kAttribute:
+      printer->Print("?");
+      break;
+  }
+
+  if (!ref.name && !ref.id) {
+    printer->Print("null");
+    return;
+  }
+
+  if (ref.private_reference) {
+    printer->Print("*");
+  }
+
+  if (ref.name) {
+    const ResourceName& name = ref.name.value();
+    if (print_package) {
+      printer->Print(name.to_string());
+    } else {
+      printer->Print(to_string(name.type));
+      printer->Print("/");
+      printer->Print(name.entry);
+    }
+  } else if (ref.id && ref.id.value().is_valid_dynamic()) {
+    printer->Print(ref.id.value().to_string());
+  }
+}
+
+void Reference::PrettyPrint(Printer* printer) const {
+  PrettyPrintReferenceImpl(*this, true /*print_package*/, printer);
+}
+
+void Reference::PrettyPrint(const StringPiece& package, Printer* printer) const {
+  const bool print_package = name ? package != name.value().package : true;
+  PrettyPrintReferenceImpl(*this, print_package, printer);
+}
+
 bool Id::Equals(const Value* value) const {
   return ValueCast<Id>(value) != nullptr;
 }
@@ -165,11 +221,16 @@
   return true;
 }
 
-Id* Id::Clone(StringPool* /*new_pool*/) const { return new Id(*this); }
+Id* Id::Clone(StringPool* /*new_pool*/) const {
+  return new Id(*this);
+}
 
-void Id::Print(std::ostream* out) const { *out << "(id)"; }
+void Id::Print(std::ostream* out) const {
+  *out << "(id)";
+}
 
-String::String(const StringPool::Ref& ref) : value(ref) {}
+String::String(const StringPool::Ref& ref) : value(ref) {
+}
 
 bool String::Equals(const Value* value) const {
   const String* other = ValueCast<String>(value);
@@ -218,7 +279,14 @@
   *out << "(string) \"" << *value << "\"";
 }
 
-StyledString::StyledString(const StringPool::StyleRef& ref) : value(ref) {}
+void String::PrettyPrint(Printer* printer) const {
+  printer->Print("\"");
+  printer->Print(*value);
+  printer->Print("\"");
+}
+
+StyledString::StyledString(const StringPool::StyleRef& ref) : value(ref) {
+}
 
 bool StyledString::Equals(const Value* value) const {
   const StyledString* other = ValueCast<StyledString>(value);
@@ -269,7 +337,8 @@
   }
 }
 
-FileReference::FileReference(const StringPool::Ref& _path) : path(_path) {}
+FileReference::FileReference(const StringPool::Ref& _path) : path(_path) {
+}
 
 bool FileReference::Equals(const Value* value) const {
   const FileReference* other = ValueCast<FileReference>(value);
@@ -302,7 +371,8 @@
   *out << "(file) " << *path;
 }
 
-BinaryPrimitive::BinaryPrimitive(const android::Res_value& val) : value(val) {}
+BinaryPrimitive::BinaryPrimitive(const android::Res_value& val) : value(val) {
+}
 
 BinaryPrimitive::BinaryPrimitive(uint8_t dataType, uint32_t data) {
   value.dataType = dataType;
@@ -318,7 +388,7 @@
          this->value.data == other->value.data;
 }
 
-bool BinaryPrimitive::Flatten(android::Res_value* out_value) const {
+bool BinaryPrimitive::Flatten(::android::Res_value* out_value) const {
   out_value->dataType = value.dataType;
   out_value->data = util::HostToDevice32(value.data);
   return true;
@@ -329,32 +399,110 @@
 }
 
 void BinaryPrimitive::Print(std::ostream* out) const {
+  *out << StringPrintf("(primitive) type=0x%02x data=0x%08x", value.dataType, value.data);
+}
+
+static std::string ComplexToString(uint32_t complex_value, bool fraction) {
+  using ::android::Res_value;
+
+  constexpr std::array<int, 4> kRadixShifts = {{23, 16, 8, 0}};
+
+  // Determine the radix that was used.
+  const uint32_t radix =
+      (complex_value >> Res_value::COMPLEX_RADIX_SHIFT) & Res_value::COMPLEX_RADIX_MASK;
+  const uint64_t mantissa = uint64_t{(complex_value >> Res_value::COMPLEX_MANTISSA_SHIFT) &
+                                     Res_value::COMPLEX_MANTISSA_MASK}
+                            << kRadixShifts[radix];
+  const float value = mantissa * (1.0f / (1 << 23));
+
+  std::string str = StringPrintf("%f", value);
+
+  const int unit_type =
+      (complex_value >> Res_value::COMPLEX_UNIT_SHIFT) & Res_value::COMPLEX_UNIT_MASK;
+  if (fraction) {
+    switch (unit_type) {
+      case Res_value::COMPLEX_UNIT_FRACTION:
+        str += "%";
+        break;
+      case Res_value::COMPLEX_UNIT_FRACTION_PARENT:
+        str += "%p";
+        break;
+      default:
+        str += "???";
+        break;
+    }
+  } else {
+    switch (unit_type) {
+      case Res_value::COMPLEX_UNIT_PX:
+        str += "px";
+        break;
+      case Res_value::COMPLEX_UNIT_DIP:
+        str += "dp";
+        break;
+      case Res_value::COMPLEX_UNIT_SP:
+        str += "sp";
+        break;
+      case Res_value::COMPLEX_UNIT_PT:
+        str += "pt";
+        break;
+      case Res_value::COMPLEX_UNIT_IN:
+        str += "in";
+        break;
+      case Res_value::COMPLEX_UNIT_MM:
+        str += "mm";
+        break;
+      default:
+        str += "???";
+        break;
+    }
+  }
+  return str;
+}
+
+void BinaryPrimitive::PrettyPrint(Printer* printer) const {
+  using ::android::Res_value;
   switch (value.dataType) {
-    case android::Res_value::TYPE_NULL:
-      if (value.data == android::Res_value::DATA_NULL_EMPTY) {
-        *out << "(empty)";
+    case Res_value::TYPE_NULL:
+      if (value.data == Res_value::DATA_NULL_EMPTY) {
+        printer->Print("@empty");
       } else {
-        *out << "(null)";
+        printer->Print("@null");
       }
       break;
-    case android::Res_value::TYPE_INT_DEC:
-      *out << "(integer) " << static_cast<int32_t>(value.data);
+
+    case Res_value::TYPE_INT_DEC:
+      printer->Print(StringPrintf("%" PRIi32, static_cast<int32_t>(value.data)));
       break;
-    case android::Res_value::TYPE_INT_HEX:
-      *out << "(integer) 0x" << std::hex << value.data << std::dec;
+
+    case Res_value::TYPE_INT_HEX:
+      printer->Print(StringPrintf("0x%08x", value.data));
       break;
-    case android::Res_value::TYPE_INT_BOOLEAN:
-      *out << "(boolean) " << (value.data != 0 ? "true" : "false");
+
+    case Res_value::TYPE_INT_BOOLEAN:
+      printer->Print(value.data != 0 ? "true" : "false");
       break;
-    case android::Res_value::TYPE_INT_COLOR_ARGB8:
-    case android::Res_value::TYPE_INT_COLOR_RGB8:
-    case android::Res_value::TYPE_INT_COLOR_ARGB4:
-    case android::Res_value::TYPE_INT_COLOR_RGB4:
-      *out << "(color) #" << std::hex << value.data << std::dec;
+
+    case Res_value::TYPE_INT_COLOR_ARGB8:
+    case Res_value::TYPE_INT_COLOR_RGB8:
+    case Res_value::TYPE_INT_COLOR_ARGB4:
+    case Res_value::TYPE_INT_COLOR_RGB4:
+      printer->Print(StringPrintf("#%08x", value.data));
       break;
+
+    case Res_value::TYPE_FLOAT:
+      printer->Print(StringPrintf("%g", *reinterpret_cast<const float*>(&value.data)));
+      break;
+
+    case Res_value::TYPE_DIMENSION:
+      printer->Print(ComplexToString(value.data, false /*fraction*/));
+      break;
+
+    case Res_value::TYPE_FRACTION:
+      printer->Print(ComplexToString(value.data, true /*fraction*/));
+      break;
+
     default:
-      *out << "(unknown 0x" << std::hex << (int)value.dataType << ") 0x"
-           << std::hex << value.data << std::dec;
+      printer->Print(StringPrintf("(unknown 0x%02x) 0x%08x", value.dataType, value.data));
       break;
   }
 }
@@ -424,107 +572,107 @@
   return new Attribute(*this);
 }
 
-void Attribute::PrintMask(std::ostream* out) const {
+std::string Attribute::MaskString() const {
   if (type_mask == android::ResTable_map::TYPE_ANY) {
-    *out << "any";
-    return;
+    return "any";
   }
 
+  std::ostringstream out;
   bool set = false;
   if ((type_mask & android::ResTable_map::TYPE_REFERENCE) != 0) {
     if (!set) {
       set = true;
     } else {
-      *out << "|";
+      out << "|";
     }
-    *out << "reference";
+    out << "reference";
   }
 
   if ((type_mask & android::ResTable_map::TYPE_STRING) != 0) {
     if (!set) {
       set = true;
     } else {
-      *out << "|";
+      out << "|";
     }
-    *out << "string";
+    out << "string";
   }
 
   if ((type_mask & android::ResTable_map::TYPE_INTEGER) != 0) {
     if (!set) {
       set = true;
     } else {
-      *out << "|";
+      out << "|";
     }
-    *out << "integer";
+    out << "integer";
   }
 
   if ((type_mask & android::ResTable_map::TYPE_BOOLEAN) != 0) {
     if (!set) {
       set = true;
     } else {
-      *out << "|";
+      out << "|";
     }
-    *out << "boolean";
+    out << "boolean";
   }
 
   if ((type_mask & android::ResTable_map::TYPE_COLOR) != 0) {
     if (!set) {
       set = true;
     } else {
-      *out << "|";
+      out << "|";
     }
-    *out << "color";
+    out << "color";
   }
 
   if ((type_mask & android::ResTable_map::TYPE_FLOAT) != 0) {
     if (!set) {
       set = true;
     } else {
-      *out << "|";
+      out << "|";
     }
-    *out << "float";
+    out << "float";
   }
 
   if ((type_mask & android::ResTable_map::TYPE_DIMENSION) != 0) {
     if (!set) {
       set = true;
     } else {
-      *out << "|";
+      out << "|";
     }
-    *out << "dimension";
+    out << "dimension";
   }
 
   if ((type_mask & android::ResTable_map::TYPE_FRACTION) != 0) {
     if (!set) {
       set = true;
     } else {
-      *out << "|";
+      out << "|";
     }
-    *out << "fraction";
+    out << "fraction";
   }
 
   if ((type_mask & android::ResTable_map::TYPE_ENUM) != 0) {
     if (!set) {
       set = true;
     } else {
-      *out << "|";
+      out << "|";
     }
-    *out << "enum";
+    out << "enum";
   }
 
   if ((type_mask & android::ResTable_map::TYPE_FLAGS) != 0) {
     if (!set) {
       set = true;
     } else {
-      *out << "|";
+      out << "|";
     }
-    *out << "flags";
+    out << "flags";
   }
+  return out.str();
 }
 
 void Attribute::Print(std::ostream* out) const {
-  *out << "(attr) ";
-  PrintMask(out);
+  *out << "(attr) " << MaskString();
 
   if (!symbols.empty()) {
     *out << " [" << util::Joiner(symbols, ", ") << "]";
diff --git a/tools/aapt2/ResourceValues.h b/tools/aapt2/ResourceValues.h
index fd242a1..b2ec8bdd 100644
--- a/tools/aapt2/ResourceValues.h
+++ b/tools/aapt2/ResourceValues.h
@@ -29,6 +29,7 @@
 #include "Resource.h"
 #include "StringPool.h"
 #include "io/File.h"
+#include "text/Printer.h"
 #include "util/Maybe.h"
 
 namespace aapt {
@@ -106,6 +107,10 @@
   // Human readable printout of this value.
   virtual void Print(std::ostream* out) const = 0;
 
+  // Human readable printout of this value that may omit some information for the sake
+  // of brevity and readability. Default implementation just calls Print().
+  virtual void PrettyPrint(text::Printer* printer) const;
+
   friend std::ostream& operator<<(std::ostream& out, const Value& value);
 
  protected:
@@ -162,6 +167,10 @@
   bool Flatten(android::Res_value* out_value) const override;
   Reference* Clone(StringPool* new_pool) const override;
   void Print(std::ostream* out) const override;
+  void PrettyPrint(text::Printer* printer) const override;
+
+  // Prints the reference without a package name if the package name matches the one given.
+  void PrettyPrint(const android::StringPiece& package, text::Printer* printer) const;
 };
 
 bool operator<(const Reference&, const Reference&);
@@ -224,6 +233,7 @@
   bool Flatten(android::Res_value* out_value) const override;
   String* Clone(StringPool* new_pool) const override;
   void Print(std::ostream* out) const override;
+  void PrettyPrint(text::Printer* printer) const override;
 };
 
 struct StyledString : public BaseItem<StyledString> {
@@ -274,6 +284,7 @@
   bool Flatten(android::Res_value* out_value) const override;
   BinaryPrimitive* Clone(StringPool* new_pool) const override;
   void Print(std::ostream* out) const override;
+  void PrettyPrint(text::Printer* printer) const override;
 };
 
 struct Attribute : public BaseValue<Attribute> {
@@ -294,7 +305,7 @@
 
   bool Equals(const Value* value) const override;
   Attribute* Clone(StringPool* new_pool) const override;
-  void PrintMask(std::ostream* out) const;
+  std::string MaskString() const;
   void Print(std::ostream* out) const override;
   bool Matches(const Item& item, DiagMessage* out_msg = nullptr) const;
 };
diff --git a/tools/aapt2/Source.h b/tools/aapt2/Source.h
index d7f2a66..0f312d6 100644
--- a/tools/aapt2/Source.h
+++ b/tools/aapt2/Source.h
@@ -20,16 +20,14 @@
 #include <ostream>
 #include <string>
 
+#include "android-base/stringprintf.h"
 #include "androidfw/StringPiece.h"
 
 #include "util/Maybe.h"
 
 namespace aapt {
 
-/**
- * Represents a file on disk. Used for logging and
- * showing errors.
- */
+// Represents a file on disk. Used for logging and showing errors.
 struct Source {
   std::string path;
   Maybe<size_t> line;
@@ -42,7 +40,16 @@
   inline Source(const android::StringPiece& path, size_t line)
       : path(path.to_string()), line(line) {}
 
-  inline Source WithLine(size_t line) const { return Source(path, line); }
+  inline Source WithLine(size_t line) const {
+    return Source(path, line);
+  }
+
+  std::string to_string() const {
+    if (line) {
+      return ::android::base::StringPrintf("%s:%zd", path.c_str(), line.value());
+    }
+    return path;
+  }
 };
 
 //
@@ -50,11 +57,7 @@
 //
 
 inline ::std::ostream& operator<<(::std::ostream& out, const Source& source) {
-  out << source.path;
-  if (source.line) {
-    out << ":" << source.line.value();
-  }
-  return out;
+  return out << source.to_string();
 }
 
 inline bool operator==(const Source& lhs, const Source& rhs) {
diff --git a/tools/aapt2/cmd/Dump.cpp b/tools/aapt2/cmd/Dump.cpp
index bc8f1dc..bc7f5a8 100644
--- a/tools/aapt2/cmd/Dump.cpp
+++ b/tools/aapt2/cmd/Dump.cpp
@@ -14,8 +14,10 @@
  * limitations under the License.
  */
 
+#include <cinttypes>
 #include <vector>
 
+#include "android-base/stringprintf.h"
 #include "androidfw/StringPiece.h"
 
 #include "Debug.h"
@@ -27,9 +29,12 @@
 #include "io/FileStream.h"
 #include "io/ZipArchive.h"
 #include "process/IResourceTableConsumer.h"
+#include "text/Printer.h"
 #include "util/Files.h"
 
+using ::aapt::text::Printer;
 using ::android::StringPiece;
+using ::android::base::StringPrintf;
 
 namespace aapt {
 
@@ -48,16 +53,28 @@
 }
 
 static void DumpCompiledFile(const ResourceFile& file, const Source& source, off64_t offset,
-                             size_t len) {
-  std::cout << "Resource: " << file.name << "\n"
-            << "Config:   " << file.config << "\n"
-            << "Source:   " << file.source << "\n"
-            << "Type:     " << ResourceFileTypeToString(file.type) << "\n"
-            << "DataOff:  " << offset << "\n"
-            << "DataLen:  " << len << "\n";
+                             size_t len, Printer* printer) {
+  printer->Print("Resource: ");
+  printer->Println(file.name.to_string());
+
+  printer->Print("Config:   ");
+  printer->Println(file.config.to_string());
+
+  printer->Print("Source:   ");
+  printer->Println(file.source.to_string());
+
+  printer->Print("Type:     ");
+  printer->Println(ResourceFileTypeToString(file.type));
+
+  printer->Println(StringPrintf("Data:     offset=%" PRIi64 " length=%zd", offset, len));
 }
 
 static bool TryDumpFile(IAaptContext* context, const std::string& file_path) {
+  // Use a smaller buffer so that there is less latency for dumping to stdout.
+  constexpr size_t kStdOutBufferSize = 1024u;
+  io::FileOutputStream fout(STDOUT_FILENO, kStdOutBufferSize);
+  Printer printer(&fout);
+
   DebugPrintTableOptions print_options;
   print_options.show_sources = true;
 
@@ -83,6 +100,8 @@
                                          << "failed to parse table: " << err);
         return false;
       }
+
+      printer.Println("Proto APK");
     } else if (io::IFile* file = zip->FindFile("resources.arsc")) {
       std::unique_ptr<io::IData> data = file->OpenAsData();
       if (!data) {
@@ -95,9 +114,11 @@
       if (!parser.Parse()) {
         return false;
       }
+
+      printer.Println("Binary APK");
     }
 
-    Debug::PrintTable(table, print_options);
+    Debug::PrintTable(table, print_options, &printer);
     return true;
   }
 
@@ -118,9 +139,12 @@
     return false;
   }
 
+  printer.Println("AAPT2 Container (APC)");
   ContainerReaderEntry* entry;
   while ((entry = reader.Next()) != nullptr) {
     if (entry->Type() == ContainerEntryType::kResTable) {
+      printer.Println("kResTable");
+
       pb::ResourceTable pb_table;
       if (!entry->GetResTable(&pb_table)) {
         context->GetDiagnostics()->Error(DiagMessage(file_path)
@@ -136,8 +160,11 @@
         continue;
       }
 
-      Debug::PrintTable(table, print_options);
+      printer.Indent();
+      Debug::PrintTable(table, print_options, &printer);
+      printer.Undent();
     } else if (entry->Type() == ContainerEntryType::kResFile) {
+      printer.Println("kResFile");
       pb::internal::CompiledFile pb_compiled_file;
       off64_t offset;
       size_t length;
@@ -155,7 +182,9 @@
         continue;
       }
 
-      DumpCompiledFile(file, Source(file_path), offset, length);
+      printer.Indent();
+      DumpCompiledFile(file, Source(file_path), offset, length, &printer);
+      printer.Undent();
     }
   }
   return true;
diff --git a/tools/aapt2/compile/InlineXmlFormatParser.cpp b/tools/aapt2/compile/InlineXmlFormatParser.cpp
index 8b6c524..238e339 100644
--- a/tools/aapt2/compile/InlineXmlFormatParser.cpp
+++ b/tools/aapt2/compile/InlineXmlFormatParser.cpp
@@ -164,7 +164,7 @@
 
     // Add the inline attribute to the parent.
     parent_el->attributes.push_back(xml::Attribute{decl.attr_namespace_uri, decl.attr_name,
-                                                   "@" + new_doc->file.name.ToString()});
+                                                   "@" + new_doc->file.name.to_string()});
 
     // Delete the subtree.
     for (auto iter = parent_el->children.begin(); iter != parent_el->children.end(); ++iter) {
diff --git a/tools/aapt2/format/binary/BinaryResourceParser.cpp b/tools/aapt2/format/binary/BinaryResourceParser.cpp
index 66510b0..5078678 100644
--- a/tools/aapt2/format/binary/BinaryResourceParser.cpp
+++ b/tools/aapt2/format/binary/BinaryResourceParser.cpp
@@ -424,7 +424,7 @@
       // We can ignore the value here.
       return util::make_unique<Id>();
     default:
-      diag_->Error(DiagMessage() << "illegal map type '" << ToString(name.type) << "' ("
+      diag_->Error(DiagMessage() << "illegal map type '" << to_string(name.type) << "' ("
                                  << (int)name.type << ")");
       break;
   }
diff --git a/tools/aapt2/format/binary/TableFlattener.cpp b/tools/aapt2/format/binary/TableFlattener.cpp
index 259f2e9..4a1b46c 100644
--- a/tools/aapt2/format/binary/TableFlattener.cpp
+++ b/tools/aapt2/format/binary/TableFlattener.cpp
@@ -472,7 +472,7 @@
         expected_type_id++;
       }
       expected_type_id++;
-      type_pool_.MakeRef(ToString(type->type));
+      type_pool_.MakeRef(to_string(type->type));
 
       std::vector<ResourceEntry*> sorted_entries = CollectAndSortEntries(type);
       if (sorted_entries.empty()) {
diff --git a/tools/aapt2/format/proto/ProtoSerialize.cpp b/tools/aapt2/format/proto/ProtoSerialize.cpp
index 1d184fe..97ce01a 100644
--- a/tools/aapt2/format/proto/ProtoSerialize.cpp
+++ b/tools/aapt2/format/proto/ProtoSerialize.cpp
@@ -284,7 +284,7 @@
       if (type->id) {
         pb_type->mutable_type_id()->set_id(type->id.value());
       }
-      pb_type->set_name(ToString(type->type).to_string());
+      pb_type->set_name(to_string(type->type).to_string());
 
       for (const std::unique_ptr<ResourceEntry>& entry : type->entries) {
         pb::Entry* pb_entry = pb_type->add_entry();
@@ -328,7 +328,7 @@
   pb_ref->set_id(ref.id.value_or_default(ResourceId(0x0)).id);
 
   if (ref.name) {
-    pb_ref->set_name(ref.name.value().ToString());
+    pb_ref->set_name(ref.name.value().to_string());
   }
 
   pb_ref->set_private_(ref.private_reference);
@@ -523,14 +523,14 @@
 }
 
 void SerializeCompiledFileToPb(const ResourceFile& file, pb::internal::CompiledFile* out_file) {
-  out_file->set_resource_name(file.name.ToString());
+  out_file->set_resource_name(file.name.to_string());
   out_file->set_source_path(file.source.path);
   out_file->set_type(SerializeFileReferenceTypeToPb(file.type));
   SerializeConfig(file.config, out_file->mutable_config());
 
   for (const SourcedResourceName& exported : file.exported_symbols) {
     pb::internal::CompiledFile_Symbol* pb_symbol = out_file->add_exported_symbol();
-    pb_symbol->set_resource_name(exported.name.ToString());
+    pb_symbol->set_resource_name(exported.name.to_string());
     pb_symbol->mutable_source()->set_line_number(exported.line);
   }
 }
diff --git a/tools/aapt2/io/FileStream.cpp b/tools/aapt2/io/FileStream.cpp
index 2f7a4b3..4ff6d78 100644
--- a/tools/aapt2/io/FileStream.cpp
+++ b/tools/aapt2/io/FileStream.cpp
@@ -26,6 +26,7 @@
 #include "android-base/utf8.h"
 
 using ::android::base::SystemErrorCodeToString;
+using ::android::base::unique_fd;
 
 namespace aapt {
 namespace io {
@@ -100,7 +101,13 @@
 }
 
 FileOutputStream::FileOutputStream(const std::string& path, int mode, size_t buffer_capacity)
-    : FileOutputStream(::android::base::utf8::open(path.c_str(), mode), buffer_capacity) {
+    : FileOutputStream(unique_fd(::android::base::utf8::open(path.c_str(), mode)),
+                       buffer_capacity) {
+}
+
+FileOutputStream::FileOutputStream(unique_fd fd, size_t buffer_capacity)
+    : FileOutputStream(fd.get(), buffer_capacity) {
+  owned_fd_ = std::move(fd);
 }
 
 FileOutputStream::FileOutputStream(int fd, size_t buffer_capacity)
@@ -118,7 +125,7 @@
 }
 
 bool FileOutputStream::Next(void** data, size_t* size) {
-  if (fd_ == -1 || HadError()) {
+  if (HadError()) {
     return false;
   }
 
@@ -159,7 +166,8 @@
   ssize_t n = TEMP_FAILURE_RETRY(write(fd_, buffer_.get(), buffer_offset_));
   if (n < 0) {
     error_ = SystemErrorCodeToString(errno);
-    fd_.reset();
+    owned_fd_.reset();
+    fd_ = -1;
     buffer_.reset();
     return false;
   }
diff --git a/tools/aapt2/io/FileStream.h b/tools/aapt2/io/FileStream.h
index 3b07667..4ed1ad5 100644
--- a/tools/aapt2/io/FileStream.h
+++ b/tools/aapt2/io/FileStream.h
@@ -29,12 +29,15 @@
 namespace aapt {
 namespace io {
 
+constexpr size_t kDefaultBufferCapacity = 4096u;
+
 class FileInputStream : public InputStream {
  public:
-  explicit FileInputStream(const std::string& path, size_t buffer_capacity = 4096);
+  explicit FileInputStream(const std::string& path,
+                           size_t buffer_capacity = kDefaultBufferCapacity);
 
-  // Takes ownership of `fd`.
-  explicit FileInputStream(int fd, size_t buffer_capacity = 4096);
+  // Take ownership of `fd`.
+  explicit FileInputStream(int fd, size_t buffer_capacity = kDefaultBufferCapacity);
 
   bool Next(const void** data, size_t* size) override;
 
@@ -61,10 +64,14 @@
 class FileOutputStream : public OutputStream {
  public:
   explicit FileOutputStream(const std::string& path, int mode = O_RDWR | O_CREAT | O_BINARY,
-                            size_t buffer_capacity = 4096);
+                            size_t buffer_capacity = kDefaultBufferCapacity);
+
+  // Does not take ownership of `fd`.
+  explicit FileOutputStream(int fd, size_t buffer_capacity = kDefaultBufferCapacity);
 
   // Takes ownership of `fd`.
-  explicit FileOutputStream(int fd, size_t buffer_capacity = 4096);
+  explicit FileOutputStream(android::base::unique_fd fd,
+                            size_t buffer_capacity = kDefaultBufferCapacity);
 
   ~FileOutputStream();
 
@@ -86,7 +93,8 @@
 
   bool FlushImpl();
 
-  android::base::unique_fd fd_;
+  android::base::unique_fd owned_fd_;
+  int fd_;
   std::string error_;
   std::unique_ptr<uint8_t[]> buffer_;
   size_t buffer_capacity_;
diff --git a/tools/aapt2/io/FileStream_test.cpp b/tools/aapt2/io/FileStream_test.cpp
index 68c3cb1..a6d58ca 100644
--- a/tools/aapt2/io/FileStream_test.cpp
+++ b/tools/aapt2/io/FileStream_test.cpp
@@ -87,10 +87,8 @@
   const std::string input = "this is a cool string";
 
   TemporaryFile file;
-  int fd = file.release();
 
-  // FileOutputStream takes ownership.
-  FileOutputStream out(fd, 10u);
+  FileOutputStream out(file.fd, 10u);
   ASSERT_FALSE(out.HadError());
   EXPECT_THAT(out.ByteCount(), Eq(0u));
 
@@ -118,10 +116,10 @@
 
   ASSERT_TRUE(out.Flush());
 
-  lseek64(fd, 0, SEEK_SET);
+  lseek64(file.fd, 0, SEEK_SET);
 
   std::string actual;
-  ASSERT_TRUE(android::base::ReadFdToString(fd, &actual));
+  ASSERT_TRUE(android::base::ReadFdToString(file.fd, &actual));
   EXPECT_THAT(actual, StrEq(input));
 }
 
diff --git a/tools/aapt2/io/Util.cpp b/tools/aapt2/io/Util.cpp
index d270340..7ee1016 100644
--- a/tools/aapt2/io/Util.cpp
+++ b/tools/aapt2/io/Util.cpp
@@ -18,6 +18,7 @@
 
 #include "google/protobuf/io/zero_copy_stream_impl_lite.h"
 
+using ::android::StringPiece;
 using ::google::protobuf::io::ZeroCopyOutputStream;
 
 namespace aapt {
@@ -93,6 +94,25 @@
   return !in->HadError();
 }
 
+bool Copy(OutputStream* out, const StringPiece& in) {
+  const char* in_buffer = in.data();
+  size_t in_len = in.size();
+  while (in_len != 0) {
+    void* out_buffer;
+    size_t out_len;
+    if (!out->Next(&out_buffer, &out_len)) {
+      return false;
+    }
+
+    const size_t bytes_to_copy = in_len < out_len ? in_len : out_len;
+    memcpy(out_buffer, in_buffer, bytes_to_copy);
+    out->BackUp(out_len - bytes_to_copy);
+    in_buffer += bytes_to_copy;
+    in_len -= bytes_to_copy;
+  }
+  return true;
+}
+
 bool Copy(ZeroCopyOutputStream* out, InputStream* in) {
   OutputStreamAdaptor adaptor(out);
   return Copy(&adaptor, in);
diff --git a/tools/aapt2/io/Util.h b/tools/aapt2/io/Util.h
index 1e48508..de2ab39 100644
--- a/tools/aapt2/io/Util.h
+++ b/tools/aapt2/io/Util.h
@@ -42,6 +42,7 @@
 // Copies the data from in to out. Returns false if there was an error.
 // If there was an error, check the individual streams' HadError/GetError methods.
 bool Copy(OutputStream* out, InputStream* in);
+bool Copy(OutputStream* out, const ::android::StringPiece& in);
 bool Copy(::google::protobuf::io::ZeroCopyOutputStream* out, InputStream* in);
 
 class OutputStreamAdaptor : public io::OutputStream {
diff --git a/tools/aapt2/java/JavaClassGenerator.cpp b/tools/aapt2/java/JavaClassGenerator.cpp
index 3ba4dd8..91cef64 100644
--- a/tools/aapt2/java/JavaClassGenerator.cpp
+++ b/tools/aapt2/java/JavaClassGenerator.cpp
@@ -461,7 +461,7 @@
   }
 
   if (out_rewrite_method != nullptr) {
-    const StringPiece& type_str = ToString(name.type);
+    const StringPiece& type_str = to_string(name.type);
     out_rewrite_method->AppendStatement(StringPrintf("%s.%s = (%s.%s & 0x00ffffff) | (p << 24);",
                                                      type_str.data(), field_name.data(),
                                                      type_str.data(), field_name.data()));
@@ -584,7 +584,7 @@
           (options_.types == JavaClassGeneratorOptions::SymbolTypes::kPublic);
 
       std::unique_ptr<ClassDefinition> class_def = util::make_unique<ClassDefinition>(
-          ToString(type->type), ClassQualifier::kStatic, force_creation_if_empty);
+          to_string(type->type), ClassQualifier::kStatic, force_creation_if_empty);
       if (!ProcessType(package_name_to_generate, *package, *type, class_def.get(),
                        rewrite_method.get(), out_r_txt)) {
         return false;
diff --git a/tools/aapt2/process/SymbolTable.cpp b/tools/aapt2/process/SymbolTable.cpp
index 882a85b..2d517c7 100644
--- a/tools/aapt2/process/SymbolTable.cpp
+++ b/tools/aapt2/process/SymbolTable.cpp
@@ -289,7 +289,7 @@
   const android::ResTable& table = assets_.getResources(false);
 
   const std::u16string package16 = util::Utf8ToUtf16(name.package);
-  const std::u16string type16 = util::Utf8ToUtf16(ToString(name.type));
+  const std::u16string type16 = util::Utf8ToUtf16(to_string(name.type));
   const std::u16string entry16 = util::Utf8ToUtf16(name.entry);
 
   uint32_t type_spec_flags = 0;
diff --git a/tools/aapt2/text/Printer.cpp b/tools/aapt2/text/Printer.cpp
new file mode 100644
index 0000000..38b3585
--- /dev/null
+++ b/tools/aapt2/text/Printer.cpp
@@ -0,0 +1,92 @@
+/*
+ * 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 "text/Printer.h"
+
+#include <algorithm>
+
+#include "io/Util.h"
+
+using ::aapt::io::OutputStream;
+using ::android::StringPiece;
+
+namespace aapt {
+namespace text {
+
+void Printer::Println(const StringPiece& str) {
+  Print(str);
+  Print("\n");
+}
+
+void Printer::Println() {
+  Print("\n");
+}
+
+void Printer::Print(const StringPiece& str) {
+  if (error_) {
+    return;
+  }
+
+  auto remaining_str_begin = str.begin();
+  const auto remaining_str_end = str.end();
+  while (remaining_str_end != remaining_str_begin) {
+    // Find the next new-line.
+    const auto new_line_iter = std::find(remaining_str_begin, remaining_str_end, '\n');
+
+    // We will copy the string up until the next new-line (or end of string).
+    const StringPiece str_to_copy = str.substr(remaining_str_begin, new_line_iter);
+    if (!str_to_copy.empty()) {
+      if (needs_indent_) {
+        for (int i = 0; i < indent_level_; i++) {
+          if (!io::Copy(out_, "  ")) {
+            error_ = true;
+            return;
+          }
+        }
+        needs_indent_ = false;
+      }
+
+      if (!io::Copy(out_, str_to_copy)) {
+        error_ = true;
+        return;
+      }
+    }
+
+    // If we found a new-line.
+    if (new_line_iter != remaining_str_end) {
+      if (!io::Copy(out_, "\n")) {
+        error_ = true;
+        return;
+      }
+      needs_indent_ = true;
+      // Ok to increment iterator here because we know that the '\n' character is one byte.
+      remaining_str_begin = new_line_iter + 1;
+    } else {
+      remaining_str_begin = new_line_iter;
+    }
+  }
+}
+
+void Printer::Indent() {
+  ++indent_level_;
+}
+
+void Printer::Undent() {
+  --indent_level_;
+}
+
+}  // namespace text
+}  // namespace aapt
diff --git a/tools/aapt2/text/Printer.h b/tools/aapt2/text/Printer.h
new file mode 100644
index 0000000..94b3c0b
--- /dev/null
+++ b/tools/aapt2/text/Printer.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 AAPT_TEXT_PRINTER_H
+#define AAPT_TEXT_PRINTER_H
+
+#include "android-base/macros.h"
+#include "androidfw/StringPiece.h"
+
+#include "io/Io.h"
+
+namespace aapt {
+namespace text {
+
+// An indenting Printer that helps write formatted text to the OutputStream.
+class Printer {
+ public:
+  explicit Printer(::aapt::io::OutputStream* out) : out_(out) {
+  }
+
+  void Print(const ::android::StringPiece& str);
+  void Println(const ::android::StringPiece& str);
+  void Println();
+
+  void Indent();
+  void Undent();
+
+ private:
+  DISALLOW_COPY_AND_ASSIGN(Printer);
+
+  ::aapt::io::OutputStream* out_;
+  int indent_level_ = 0;
+  bool needs_indent_ = false;
+  bool error_ = false;
+};
+
+}  // namespace text
+}  // namespace aapt
+
+#endif  // AAPT_TEXT_PRINTER_H
diff --git a/tools/aapt2/text/Printer_test.cpp b/tools/aapt2/text/Printer_test.cpp
new file mode 100644
index 0000000..58beae7
--- /dev/null
+++ b/tools/aapt2/text/Printer_test.cpp
@@ -0,0 +1,67 @@
+/*
+ * 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 "text/Printer.h"
+
+#include "io/StringStream.h"
+#include "test/Test.h"
+
+using ::aapt::io::StringOutputStream;
+using ::android::StringPiece;
+using ::testing::StrEq;
+
+namespace aapt {
+namespace text {
+
+TEST(PrinterTest, PrintsToStreamWithIndents) {
+  std::string result;
+  StringOutputStream out(&result);
+  Printer printer(&out);
+
+  printer.Print("Hello");
+  out.Flush();
+  EXPECT_THAT(result, StrEq("Hello"));
+
+  printer.Println();
+  out.Flush();
+  EXPECT_THAT(result, StrEq("Hello\n"));
+
+  // This shouldn't print anything yet.
+  printer.Indent();
+  out.Flush();
+  EXPECT_THAT(result, StrEq("Hello\n"));
+
+  // Now we should see the indent.
+  printer.Print("world!");
+  out.Flush();
+  EXPECT_THAT(result, StrEq("Hello\n  world!"));
+
+  printer.Println(" What a\nlovely day.");
+  out.Flush();
+  EXPECT_THAT(result, StrEq("Hello\n  world! What a\n  lovely day.\n"));
+
+  // This shouldn't print anything yet.
+  printer.Undent();
+  out.Flush();
+  EXPECT_THAT(result, StrEq("Hello\n  world! What a\n  lovely day.\n"));
+
+  printer.Println("Isn't it?");
+  out.Flush();
+  EXPECT_THAT(result, StrEq("Hello\n  world! What a\n  lovely day.\nIsn't it?\n"));
+}
+
+}  // namespace text
+}  // namespace aapt