AAPT2: Move all file output to FileOutputStream

FileOutputStream is safe to use on Windows, as it opens
files using our compatibility API.

Bug: 68262818
Test: make aapt2_tests
Change-Id: Ib0b27e93edd609b49b1327db7d9867a002198ebb
diff --git a/tools/aapt2/Resource.h b/tools/aapt2/Resource.h
index 96a0203..6fcf0f6 100644
--- a/tools/aapt2/Resource.h
+++ b/tools/aapt2/Resource.h
@@ -268,6 +268,11 @@
   return out << res_id.to_string();
 }
 
+// For generic code to call 'using std::to_string; to_string(T);'.
+inline std::string to_string(const ResourceId& id) {
+  return id.to_string();
+}
+
 //
 // ResourceType implementation.
 //
diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp
index 66b5a7a..e94c0b4 100644
--- a/tools/aapt2/cmd/Link.cpp
+++ b/tools/aapt2/cmd/Link.cpp
@@ -16,7 +16,6 @@
 
 #include <sys/stat.h>
 
-#include <fstream>
 #include <queue>
 #include <unordered_map>
 #include <vector>
@@ -212,6 +211,8 @@
 // This delegate will attempt to masquerade any '@id/' references with ID 0xPPTTEEEE,
 // where PP > 7f, as 0x7fPPEEEE. Any potential overlapping is verified and an error occurs if such
 // an overlap exists.
+//
+// See b/37498913.
 class FeatureSplitSymbolTableDelegate : public DefaultSymbolTableDelegate {
  public:
   FeatureSplitSymbolTableDelegate(IAaptContext* context) : context_(context) {
@@ -652,24 +653,26 @@
 static bool WriteStableIdMapToPath(IDiagnostics* diag,
                                    const std::unordered_map<ResourceName, ResourceId>& id_map,
                                    const std::string& id_map_path) {
-  std::ofstream fout(id_map_path, std::ofstream::binary);
-  if (!fout) {
-    diag->Error(DiagMessage(id_map_path) << strerror(errno));
+  io::FileOutputStream fout(id_map_path);
+  if (fout.HadError()) {
+    diag->Error(DiagMessage(id_map_path) << "failed to open: " << fout.GetError());
     return false;
   }
 
+  text::Printer printer(&fout);
   for (const auto& entry : id_map) {
     const ResourceName& name = entry.first;
     const ResourceId& id = entry.second;
-    fout << name << " = " << id << "\n";
+    printer.Print(name.to_string());
+    printer.Print(" = ");
+    printer.Println(id.to_string());
   }
+  fout.Flush();
 
-  if (!fout) {
-    diag->Error(DiagMessage(id_map_path) << "failed writing to file: "
-                                         << android::base::SystemErrorCodeToString(errno));
+  if (fout.HadError()) {
+    diag->Error(DiagMessage(id_map_path) << "failed writing to file: " << fout.GetError());
     return false;
   }
-
   return true;
 }
 
@@ -985,36 +988,35 @@
 
     file::AppendPath(&out_path, "R.java");
 
-    std::ofstream fout(out_path, std::ofstream::binary);
-    if (!fout) {
-      context_->GetDiagnostics()->Error(DiagMessage()
-                                        << "failed writing to '" << out_path
-                                        << "': " << android::base::SystemErrorCodeToString(errno));
+    io::FileOutputStream fout(out_path);
+    if (fout.HadError()) {
+      context_->GetDiagnostics()->Error(DiagMessage() << "failed writing to '" << out_path
+                                                      << "': " << fout.GetError());
       return false;
     }
 
-    std::unique_ptr<std::ofstream> fout_text;
+    std::unique_ptr<io::FileOutputStream> fout_text;
     if (out_text_symbols_path) {
-      fout_text =
-          util::make_unique<std::ofstream>(out_text_symbols_path.value(), std::ofstream::binary);
-      if (!*fout_text) {
-        context_->GetDiagnostics()->Error(
-            DiagMessage() << "failed writing to '" << out_text_symbols_path.value()
-                          << "': " << android::base::SystemErrorCodeToString(errno));
+      fout_text = util::make_unique<io::FileOutputStream>(out_text_symbols_path.value());
+      if (fout_text->HadError()) {
+        context_->GetDiagnostics()->Error(DiagMessage()
+                                          << "failed writing to '" << out_text_symbols_path.value()
+                                          << "': " << fout_text->GetError());
         return false;
       }
     }
 
     JavaClassGenerator generator(context_, table, java_options);
     if (!generator.Generate(package_name_to_generate, out_package, &fout, fout_text.get())) {
-      context_->GetDiagnostics()->Error(DiagMessage(out_path) << generator.getError());
+      context_->GetDiagnostics()->Error(DiagMessage(out_path) << generator.GetError());
       return false;
     }
 
-    if (!fout) {
-      context_->GetDiagnostics()->Error(DiagMessage()
-                                        << "failed writing to '" << out_path
-                                        << "': " << android::base::SystemErrorCodeToString(errno));
+    fout.Flush();
+
+    if (fout.HadError()) {
+      context_->GetDiagnostics()->Error(DiagMessage() << "failed writing to '" << out_path
+                                                      << "': " << fout.GetError());
       return false;
     }
     return true;
@@ -1142,18 +1144,19 @@
 
     file::AppendPath(&out_path, "Manifest.java");
 
-    std::ofstream fout(out_path, std::ofstream::binary);
-    if (!fout) {
-      context_->GetDiagnostics()->Error(DiagMessage()
-                                        << "failed writing to '" << out_path
-                                        << "': " << android::base::SystemErrorCodeToString(errno));
+    io::FileOutputStream fout(out_path);
+    if (fout.HadError()) {
+      context_->GetDiagnostics()->Error(DiagMessage() << "failed to open '" << out_path
+                                                      << "': " << fout.GetError());
       return false;
     }
 
-    if (!ClassDefinition::WriteJavaFile(manifest_class.get(), package_utf8, true, &fout)) {
-      context_->GetDiagnostics()->Error(DiagMessage()
-                                        << "failed writing to '" << out_path
-                                        << "': " << android::base::SystemErrorCodeToString(errno));
+    ClassDefinition::WriteJavaFile(manifest_class.get(), package_utf8, true, &fout);
+    fout.Flush();
+
+    if (fout.HadError()) {
+      context_->GetDiagnostics()->Error(DiagMessage() << "failed writing to '" << out_path
+                                                      << "': " << fout.GetError());
       return false;
     }
     return true;
@@ -1165,19 +1168,19 @@
     }
 
     const std::string& out_path = out.value();
-    std::ofstream fout(out_path, std::ofstream::binary);
-    if (!fout) {
-      context_->GetDiagnostics()->Error(DiagMessage()
-                                        << "failed to open '" << out_path
-                                        << "': " << android::base::SystemErrorCodeToString(errno));
+    io::FileOutputStream fout(out_path);
+    if (fout.HadError()) {
+      context_->GetDiagnostics()->Error(DiagMessage() << "failed to open '" << out_path
+                                                      << "': " << fout.GetError());
       return false;
     }
 
-    proguard::WriteKeepSet(&fout, keep_set);
-    if (!fout) {
-      context_->GetDiagnostics()->Error(DiagMessage()
-                                        << "failed writing to '" << out_path
-                                        << "': " << android::base::SystemErrorCodeToString(errno));
+    proguard::WriteKeepSet(keep_set, &fout);
+    fout.Flush();
+
+    if (fout.HadError()) {
+      context_->GetDiagnostics()->Error(DiagMessage() << "failed writing to '" << out_path
+                                                      << "': " << fout.GetError());
       return false;
     }
     return true;
diff --git a/tools/aapt2/io/FileStream.cpp b/tools/aapt2/io/FileStream.cpp
index 4ff6d78..27529bc 100644
--- a/tools/aapt2/io/FileStream.cpp
+++ b/tools/aapt2/io/FileStream.cpp
@@ -25,6 +25,12 @@
 #include "android-base/macros.h"
 #include "android-base/utf8.h"
 
+#if defined(_WIN32)
+// This is only needed for O_CLOEXEC.
+#include <windows.h>
+#define O_CLOEXEC O_NOINHERIT
+#endif
+
 using ::android::base::SystemErrorCodeToString;
 using ::android::base::unique_fd;
 
@@ -32,18 +38,20 @@
 namespace io {
 
 FileInputStream::FileInputStream(const std::string& path, size_t buffer_capacity)
-    : FileInputStream(::android::base::utf8::open(path.c_str(), O_RDONLY | O_BINARY),
-                      buffer_capacity) {
+    : buffer_capacity_(buffer_capacity) {
+  int mode = O_RDONLY | O_CLOEXEC | O_BINARY;
+  fd_.reset(TEMP_FAILURE_RETRY(::android::base::utf8::open(path.c_str(), mode)));
+  if (fd_ == -1) {
+    error_ = SystemErrorCodeToString(errno);
+  } else {
+    buffer_.reset(new uint8_t[buffer_capacity_]);
+  }
 }
 
 FileInputStream::FileInputStream(int fd, size_t buffer_capacity)
-    : fd_(fd),
-      buffer_capacity_(buffer_capacity),
-      buffer_offset_(0u),
-      buffer_size_(0u),
-      total_byte_count_(0u) {
-  if (fd_ == -1) {
-    error_ = SystemErrorCodeToString(errno);
+    : fd_(fd), buffer_capacity_(buffer_capacity) {
+  if (fd_ < 0) {
+    error_ = "Bad File Descriptor";
   } else {
     buffer_.reset(new uint8_t[buffer_capacity_]);
   }
@@ -100,9 +108,16 @@
   return error_;
 }
 
-FileOutputStream::FileOutputStream(const std::string& path, int mode, size_t buffer_capacity)
-    : FileOutputStream(unique_fd(::android::base::utf8::open(path.c_str(), mode)),
-                       buffer_capacity) {
+FileOutputStream::FileOutputStream(const std::string& path, size_t buffer_capacity)
+    : buffer_capacity_(buffer_capacity) {
+  int mode = O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC | O_BINARY;
+  owned_fd_.reset(TEMP_FAILURE_RETRY(::android::base::utf8::open(path.c_str(), mode, 0666)));
+  fd_ = owned_fd_.get();
+  if (fd_ < 0) {
+    error_ = SystemErrorCodeToString(errno);
+  } else {
+    buffer_.reset(new uint8_t[buffer_capacity_]);
+  }
 }
 
 FileOutputStream::FileOutputStream(unique_fd fd, size_t buffer_capacity)
@@ -111,9 +126,9 @@
 }
 
 FileOutputStream::FileOutputStream(int fd, size_t buffer_capacity)
-    : fd_(fd), buffer_capacity_(buffer_capacity), buffer_offset_(0u), total_byte_count_(0u) {
-  if (fd_ == -1) {
-    error_ = SystemErrorCodeToString(errno);
+    : fd_(fd), buffer_capacity_(buffer_capacity) {
+  if (fd_ < 0) {
+    error_ = "Bad File Descriptor";
   } else {
     buffer_.reset(new uint8_t[buffer_capacity_]);
   }
diff --git a/tools/aapt2/io/FileStream.h b/tools/aapt2/io/FileStream.h
index 4ed1ad5..62d910f 100644
--- a/tools/aapt2/io/FileStream.h
+++ b/tools/aapt2/io/FileStream.h
@@ -22,7 +22,6 @@
 #include <memory>
 #include <string>
 
-#include "android-base/file.h"  // for O_BINARY
 #include "android-base/macros.h"
 #include "android-base/unique_fd.h"
 
@@ -55,15 +54,15 @@
   android::base::unique_fd fd_;
   std::string error_;
   std::unique_ptr<uint8_t[]> buffer_;
-  size_t buffer_capacity_;
-  size_t buffer_offset_;
-  size_t buffer_size_;
-  size_t total_byte_count_;
+  size_t buffer_capacity_ = 0u;
+  size_t buffer_offset_ = 0u;
+  size_t buffer_size_ = 0u;
+  size_t total_byte_count_ = 0u;
 };
 
 class FileOutputStream : public OutputStream {
  public:
-  explicit FileOutputStream(const std::string& path, int mode = O_RDWR | O_CREAT | O_BINARY,
+  explicit FileOutputStream(const std::string& path,
                             size_t buffer_capacity = kDefaultBufferCapacity);
 
   // Does not take ownership of `fd`.
@@ -97,9 +96,9 @@
   int fd_;
   std::string error_;
   std::unique_ptr<uint8_t[]> buffer_;
-  size_t buffer_capacity_;
-  size_t buffer_offset_;
-  size_t total_byte_count_;
+  size_t buffer_capacity_ = 0u;
+  size_t buffer_offset_ = 0u;
+  size_t total_byte_count_ = 0u;
 };
 
 }  // namespace io
diff --git a/tools/aapt2/io/FileStream_test.cpp b/tools/aapt2/io/FileStream_test.cpp
index a6d58ca..c0eaa8e 100644
--- a/tools/aapt2/io/FileStream_test.cpp
+++ b/tools/aapt2/io/FileStream_test.cpp
@@ -16,6 +16,7 @@
 
 #include "io/FileStream.h"
 
+#include "android-base/file.h"
 #include "android-base/macros.h"
 #include "android-base/test_utils.h"
 
diff --git a/tools/aapt2/java/AnnotationProcessor.cpp b/tools/aapt2/java/AnnotationProcessor.cpp
index c93461a..8d91b00 100644
--- a/tools/aapt2/java/AnnotationProcessor.cpp
+++ b/tools/aapt2/java/AnnotationProcessor.cpp
@@ -23,6 +23,7 @@
 #include "text/Utf8Iterator.h"
 #include "util/Util.h"
 
+using ::aapt::text::Printer;
 using ::aapt::text::Utf8Iterator;
 using ::android::StringPiece;
 
@@ -109,23 +110,22 @@
   }
 }
 
-void AnnotationProcessor::WriteToStream(const StringPiece& prefix, std::ostream* out) const {
+void AnnotationProcessor::Print(Printer* printer) const {
   if (has_comments_) {
     std::string result = comment_.str();
     for (StringPiece line : util::Tokenize(result, '\n')) {
-      *out << prefix << line << "\n";
+      printer->Println(line);
     }
-    *out << prefix << " */"
-         << "\n";
+    printer->Println(" */");
   }
 
   if (annotation_bit_mask_ & AnnotationRule::kDeprecated) {
-    *out << prefix << "@Deprecated\n";
+    printer->Println("@Deprecated");
   }
 
   for (const AnnotationRule& rule : sAnnotationRules) {
     if (annotation_bit_mask_ & rule.bit_mask) {
-      *out << prefix << rule.annotation << "\n";
+      printer->Println(rule.annotation);
     }
   }
 }
diff --git a/tools/aapt2/java/AnnotationProcessor.h b/tools/aapt2/java/AnnotationProcessor.h
index a7bf73f..ae7bdb0 100644
--- a/tools/aapt2/java/AnnotationProcessor.h
+++ b/tools/aapt2/java/AnnotationProcessor.h
@@ -22,6 +22,8 @@
 
 #include "androidfw/StringPiece.h"
 
+#include "text/Printer.h"
+
 namespace aapt {
 
 // Builds a JavaDoc comment from a set of XML comments.
@@ -61,8 +63,8 @@
 
   void AppendNewLine();
 
-  // Writes the comments and annotations to the stream, with the given prefix before each line.
-  void WriteToStream(const android::StringPiece& prefix, std::ostream* out) const;
+  // Writes the comments and annotations to the Printer.
+  void Print(text::Printer* printer) const;
 
  private:
   std::stringstream comment_;
diff --git a/tools/aapt2/java/AnnotationProcessor_test.cpp b/tools/aapt2/java/AnnotationProcessor_test.cpp
index 856f4cc..69f49c8 100644
--- a/tools/aapt2/java/AnnotationProcessor_test.cpp
+++ b/tools/aapt2/java/AnnotationProcessor_test.cpp
@@ -16,8 +16,12 @@
 
 #include "java/AnnotationProcessor.h"
 
+#include "io/StringStream.h"
 #include "test/Test.h"
+#include "text/Printer.h"
 
+using ::aapt::io::StringOutputStream;
+using ::aapt::text::Printer;
 using ::testing::Eq;
 using ::testing::HasSubstr;
 using ::testing::Not;
@@ -33,9 +37,11 @@
   AnnotationProcessor processor;
   processor.AppendComment(comment);
 
-  std::stringstream result;
-  processor.WriteToStream("", &result);
-  std::string annotations = result.str();
+  std::string annotations;
+  StringOutputStream out(&annotations);
+  Printer printer(&out);
+  processor.Print(&printer);
+  out.Flush();
 
   EXPECT_THAT(annotations, HasSubstr("@Deprecated"));
 }
@@ -44,9 +50,11 @@
   AnnotationProcessor processor;
   processor.AppendComment("@SystemApi This is a system API");
 
-  std::stringstream result;
-  processor.WriteToStream("", &result);
-  std::string annotations = result.str();
+  std::string annotations;
+  StringOutputStream out(&annotations);
+  Printer printer(&out);
+  processor.Print(&printer);
+  out.Flush();
 
   EXPECT_THAT(annotations, HasSubstr("@android.annotation.SystemApi"));
   EXPECT_THAT(annotations, Not(HasSubstr("@SystemApi")));
@@ -57,9 +65,11 @@
   AnnotationProcessor processor;
   processor.AppendComment("@TestApi This is a test API");
 
-  std::stringstream result;
-  processor.WriteToStream("", &result);
-  std::string annotations = result.str();
+  std::string annotations;
+  StringOutputStream out(&annotations);
+  Printer printer(&out);
+  processor.Print(&printer);
+  out.Flush();
 
   EXPECT_THAT(annotations, HasSubstr("@android.annotation.TestApi"));
   EXPECT_THAT(annotations, Not(HasSubstr("@TestApi")));
diff --git a/tools/aapt2/java/ClassDefinition.cpp b/tools/aapt2/java/ClassDefinition.cpp
index 0c57e7e0..b692ccf 100644
--- a/tools/aapt2/java/ClassDefinition.cpp
+++ b/tools/aapt2/java/ClassDefinition.cpp
@@ -18,25 +18,27 @@
 
 #include "androidfw/StringPiece.h"
 
+using ::aapt::text::Printer;
 using ::android::StringPiece;
 
 namespace aapt {
 
-void ClassMember::WriteToStream(const StringPiece& prefix, bool final, std::ostream* out) const {
-  processor_.WriteToStream(prefix, out);
+void ClassMember::Print(bool /*final*/, Printer* printer) const {
+  processor_.Print(printer);
 }
 
 void MethodDefinition::AppendStatement(const StringPiece& statement) {
   statements_.push_back(statement.to_string());
 }
 
-void MethodDefinition::WriteToStream(const StringPiece& prefix, bool final,
-                                     std::ostream* out) const {
-  *out << prefix << signature_ << " {\n";
+void MethodDefinition::Print(bool final, Printer* printer) const {
+  printer->Print(signature_).Println(" {");
+  printer->Indent();
   for (const auto& statement : statements_) {
-    *out << prefix << "  " << statement << "\n";
+    printer->Println(statement);
   }
-  *out << prefix << "}";
+  printer->Undent();
+  printer->Print("}");
 }
 
 ClassDefinition::Result ClassDefinition::AddMember(std::unique_ptr<ClassMember> member) {
@@ -62,34 +64,32 @@
   return true;
 }
 
-void ClassDefinition::WriteToStream(const StringPiece& prefix, bool final,
-                                    std::ostream* out) const {
+void ClassDefinition::Print(bool final, Printer* printer) const {
   if (empty() && !create_if_empty_) {
     return;
   }
 
-  ClassMember::WriteToStream(prefix, final, out);
+  ClassMember::Print(final, printer);
 
-  *out << prefix << "public ";
+  printer->Print("public ");
   if (qualifier_ == ClassQualifier::kStatic) {
-    *out << "static ";
+    printer->Print("static ");
   }
-  *out << "final class " << name_ << " {\n";
-
-  std::string new_prefix = prefix.to_string();
-  new_prefix.append(kIndent);
+  printer->Print("final class ").Print(name_).Println(" {");
+  printer->Indent();
 
   for (const std::unique_ptr<ClassMember>& member : ordered_members_) {
     // There can be nullptr members when a member is added to the ClassDefinition
     // and takes precedence over a previous member with the same name. The overridden member is
     // set to nullptr.
     if (member != nullptr) {
-      member->WriteToStream(new_prefix, final, out);
-      *out << "\n";
+      member->Print(final, printer);
+      printer->Println();
     }
   }
 
-  *out << prefix << "}";
+  printer->Undent();
+  printer->Print("}");
 }
 
 constexpr static const char* sWarningHeader =
@@ -100,11 +100,12 @@
     " * should not be modified by hand.\n"
     " */\n\n";
 
-bool ClassDefinition::WriteJavaFile(const ClassDefinition* def, const StringPiece& package,
-                                    bool final, std::ostream* out) {
-  *out << sWarningHeader << "package " << package << ";\n\n";
-  def->WriteToStream("", final, out);
-  return bool(*out);
+void ClassDefinition::WriteJavaFile(const ClassDefinition* def, const StringPiece& package,
+                                    bool final, io::OutputStream* out) {
+  Printer printer(out);
+  printer.Print(sWarningHeader).Print("package ").Print(package).Println(";");
+  printer.Println();
+  def->Print(final, &printer);
 }
 
 }  // namespace aapt
diff --git a/tools/aapt2/java/ClassDefinition.h b/tools/aapt2/java/ClassDefinition.h
index 28a3489..fb11266 100644
--- a/tools/aapt2/java/ClassDefinition.h
+++ b/tools/aapt2/java/ClassDefinition.h
@@ -17,7 +17,6 @@
 #ifndef AAPT_JAVA_CLASSDEFINITION_H
 #define AAPT_JAVA_CLASSDEFINITION_H
 
-#include <ostream>
 #include <string>
 #include <unordered_map>
 #include <vector>
@@ -27,6 +26,7 @@
 
 #include "Resource.h"
 #include "java/AnnotationProcessor.h"
+#include "text/Printer.h"
 #include "util/Util.h"
 
 namespace aapt {
@@ -47,11 +47,10 @@
 
   virtual const std::string& GetName() const = 0;
 
-  // Writes the class member to the out stream. Subclasses should derive this method
+  // Writes the class member to the Printer. Subclasses should derive this method
   // to write their own data. Call this base method from the subclass to write out
   // this member's comments/annotations.
-  virtual void WriteToStream(const android::StringPiece& prefix, bool final,
-                             std::ostream* out) const;
+  virtual void Print(bool final, text::Printer* printer) const;
 
  private:
   AnnotationProcessor processor_;
@@ -71,11 +70,16 @@
     return name_;
   }
 
-  void WriteToStream(const android::StringPiece& prefix, bool final,
-                     std::ostream* out) const override {
-    ClassMember::WriteToStream(prefix, final, out);
-    *out << prefix << "public static " << (final ? "final " : "") << "int " << name_ << "=" << val_
-         << ";";
+  void Print(bool final, text::Printer* printer) const override {
+    using std::to_string;
+
+    ClassMember::Print(final, printer);
+
+    printer->Print("public static ");
+    if (final) {
+      printer->Print("final ");
+    }
+    printer->Print("int ").Print(name_).Print("=").Print(to_string(val_)).Print(";");
   }
 
  private:
@@ -100,12 +104,14 @@
     return name_;
   }
 
-  void WriteToStream(const android::StringPiece& prefix, bool final,
-                     std::ostream* out) const override {
-    ClassMember::WriteToStream(prefix, final, out);
+  void Print(bool final, text::Printer* printer) const override {
+    ClassMember::Print(final, printer);
 
-    *out << prefix << "public static " << (final ? "final " : "") << "String "
-         << name_ << "=\"" << val_ << "\";";
+    printer->Print("public static ");
+    if (final) {
+      printer->Print("final ");
+    }
+    printer->Print("String ").Print(name_).Print("=\"").Print(val_).Print("\";");
   }
 
  private:
@@ -136,25 +142,27 @@
     return name_;
   }
 
-  void WriteToStream(const android::StringPiece& prefix, bool final,
-                     std::ostream* out) const override {
-    ClassMember::WriteToStream(prefix, final, out);
+  void Print(bool final, text::Printer* printer) const override {
+    ClassMember::Print(final, printer);
 
-    *out << prefix << "public static final int[] " << name_ << "={";
+    printer->Print("public static final int[] ").Print(name_).Print("={");
+    printer->Indent();
 
     const auto begin = elements_.begin();
     const auto end = elements_.end();
     for (auto current = begin; current != end; ++current) {
       if (std::distance(begin, current) % kAttribsPerLine == 0) {
-        *out << "\n" << prefix << kIndent << kIndent;
+        printer->Println();
       }
 
-      *out << *current;
+      printer->Print(to_string(*current));
       if (std::distance(current, end) > 1) {
-        *out << ", ";
+        printer->Print(", ");
       }
     }
-    *out << "\n" << prefix << kIndent << "};";
+    printer->Println();
+    printer->Undent();
+    printer->Print("};");
   }
 
  private:
@@ -187,8 +195,7 @@
     return false;
   }
 
-  void WriteToStream(const android::StringPiece& prefix, bool final,
-                     std::ostream* out) const override;
+  void Print(bool final, text::Printer* printer) const override;
 
  private:
   DISALLOW_COPY_AND_ASSIGN(MethodDefinition);
@@ -201,8 +208,8 @@
 
 class ClassDefinition : public ClassMember {
  public:
-  static bool WriteJavaFile(const ClassDefinition* def, const android::StringPiece& package,
-                            bool final, std::ostream* out);
+  static void WriteJavaFile(const ClassDefinition* def, const android::StringPiece& package,
+                            bool final, io::OutputStream* out);
 
   ClassDefinition(const android::StringPiece& name, ClassQualifier qualifier, bool createIfEmpty)
       : name_(name.to_string()), qualifier_(qualifier), create_if_empty_(createIfEmpty) {}
@@ -220,8 +227,7 @@
     return name_;
   }
 
-  void WriteToStream(const android::StringPiece& prefix, bool final,
-                     std::ostream* out) const override;
+  void Print(bool final, text::Printer* printer) const override;
 
  private:
   DISALLOW_COPY_AND_ASSIGN(ClassDefinition);
diff --git a/tools/aapt2/java/JavaClassGenerator.cpp b/tools/aapt2/java/JavaClassGenerator.cpp
index 91cef64..9861770 100644
--- a/tools/aapt2/java/JavaClassGenerator.cpp
+++ b/tools/aapt2/java/JavaClassGenerator.cpp
@@ -37,8 +37,10 @@
 #include "java/ClassDefinition.h"
 #include "process/SymbolTable.h"
 
-using android::StringPiece;
-using android::base::StringPrintf;
+using ::aapt::io::OutputStream;
+using ::aapt::text::Printer;
+using ::android::StringPiece;
+using ::android::base::StringPrintf;
 
 namespace aapt {
 
@@ -230,7 +232,7 @@
                                           const StringPiece& package_name_to_generate,
                                           ClassDefinition* out_class_def,
                                           MethodDefinition* out_rewrite_method,
-                                          std::ostream* out_r_txt) {
+                                          Printer* r_txt_printer) {
   const std::string array_field_name = TransformToFieldName(name.entry);
   std::unique_ptr<ResourceArrayMember> array_def =
       util::make_unique<ResourceArrayMember>(array_field_name);
@@ -323,8 +325,8 @@
     array_def->GetCommentBuilder()->AppendComment(styleable_comment.str());
   }
 
-  if (out_r_txt != nullptr) {
-    *out_r_txt << "int[] styleable " << array_field_name << " {";
+  if (r_txt_printer != nullptr) {
+    r_txt_printer->Print("int[] styleable ").Print(array_field_name).Print(" {");
   }
 
   // Add the ResourceIds to the array member.
@@ -332,16 +334,16 @@
     const ResourceId id = sorted_attributes[i].attr_ref->id.value_or_default(ResourceId(0));
     array_def->AddElement(id);
 
-    if (out_r_txt != nullptr) {
+    if (r_txt_printer != nullptr) {
       if (i != 0) {
-        *out_r_txt << ",";
+        r_txt_printer->Print(",");
       }
-      *out_r_txt << " " << id;
+      r_txt_printer->Print(" ").Print(id.to_string());
     }
   }
 
-  if (out_r_txt != nullptr) {
-    *out_r_txt << " }\n";
+  if (r_txt_printer != nullptr) {
+    r_txt_printer->Println(" }");
   }
 
   // Add the Styleable array to the Styleable class.
@@ -396,9 +398,9 @@
     attr_processor->AppendComment(
         StringPrintf("@attr name %s:%s", package_name.data(), attr_name.entry.data()));
 
-    if (out_r_txt != nullptr) {
-      *out_r_txt << StringPrintf("int styleable %s %d\n", sorted_attributes[i].field_name.data(),
-                                 (int)i);
+    if (r_txt_printer != nullptr) {
+      r_txt_printer->Println(
+          StringPrintf("int styleable %s %zd", sorted_attributes[i].field_name.c_str(), i));
     }
 
     out_class_def->AddMember(std::move(index_member));
@@ -422,10 +424,12 @@
 void JavaClassGenerator::ProcessResource(const ResourceNameRef& name, const ResourceId& id,
                                          const ResourceEntry& entry, ClassDefinition* out_class_def,
                                          MethodDefinition* out_rewrite_method,
-                                         std::ostream* out_r_txt) {
+                                         text::Printer* r_txt_printer) {
   ResourceId real_id = id;
   if (context_->GetMinSdkVersion() < SDK_O && name.type == ResourceType::kId &&
       id.package_id() > kAppPackageId) {
+    // Workaround for feature splits using package IDs > 0x7F.
+    // See b/37498913.
     real_id = ResourceId(kAppPackageId, id.package_id(), id.entry_id());
   }
 
@@ -456,8 +460,13 @@
 
   out_class_def->AddMember(std::move(resource_member));
 
-  if (out_r_txt != nullptr) {
-    *out_r_txt << "int " << name.type << " " << field_name << " " << real_id << "\n";
+  if (r_txt_printer != nullptr) {
+    r_txt_printer->Print("int ")
+        .Print(to_string(name.type))
+        .Print(" ")
+        .Print(field_name)
+        .Print(" ")
+        .Println(real_id.to_string());
   }
 
   if (out_rewrite_method != nullptr) {
@@ -497,7 +506,7 @@
                                      const ResourceTableType& type,
                                      ClassDefinition* out_type_class_def,
                                      MethodDefinition* out_rewrite_method_def,
-                                     std::ostream* out_r_txt) {
+                                     Printer* r_txt_printer) {
   for (const auto& entry : type.entries) {
     const Maybe<std::string> unmangled_name =
         UnmangleResource(package.name, package_name_to_generate, *entry);
@@ -532,18 +541,18 @@
           static_cast<const Styleable*>(entry->values.front()->value.get());
 
       ProcessStyleable(resource_name, id, *styleable, package_name_to_generate, out_type_class_def,
-                       out_rewrite_method_def, out_r_txt);
+                       out_rewrite_method_def, r_txt_printer);
     } else {
       ProcessResource(resource_name, id, *entry, out_type_class_def, out_rewrite_method_def,
-                      out_r_txt);
+                      r_txt_printer);
     }
   }
   return true;
 }
 
-bool JavaClassGenerator::Generate(const StringPiece& package_name_to_generate, std::ostream* out,
-                                  std::ostream* out_r_txt) {
-  return Generate(package_name_to_generate, package_name_to_generate, out);
+bool JavaClassGenerator::Generate(const StringPiece& package_name_to_generate, OutputStream* out,
+                                  OutputStream* out_r_txt) {
+  return Generate(package_name_to_generate, package_name_to_generate, out, out_r_txt);
 }
 
 static void AppendJavaDocAnnotations(const std::vector<std::string>& annotations,
@@ -556,11 +565,16 @@
 }
 
 bool JavaClassGenerator::Generate(const StringPiece& package_name_to_generate,
-                                  const StringPiece& out_package_name, std::ostream* out,
-                                  std::ostream* out_r_txt) {
+                                  const StringPiece& out_package_name, OutputStream* out,
+                                  OutputStream* out_r_txt) {
   ClassDefinition r_class("R", ClassQualifier::kNone, true);
   std::unique_ptr<MethodDefinition> rewrite_method;
 
+  std::unique_ptr<Printer> r_txt_printer;
+  if (out_r_txt != nullptr) {
+    r_txt_printer = util::make_unique<Printer>(out_r_txt);
+  }
+
   // Generate an onResourcesLoaded() callback if requested.
   if (options_.rewrite_callback_options) {
     rewrite_method =
@@ -586,7 +600,7 @@
       std::unique_ptr<ClassDefinition> class_def = util::make_unique<ClassDefinition>(
           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)) {
+                       rewrite_method.get(), r_txt_printer.get())) {
         return false;
       }
 
@@ -595,7 +609,7 @@
         const ResourceTableType* priv_type = package->FindType(ResourceType::kAttrPrivate);
         if (priv_type) {
           if (!ProcessType(package_name_to_generate, *package, *priv_type, class_def.get(),
-                           rewrite_method.get(), out_r_txt)) {
+                           rewrite_method.get(), r_txt_printer.get())) {
             return false;
           }
         }
@@ -619,22 +633,7 @@
   }
 
   AppendJavaDocAnnotations(options_.javadoc_annotations, r_class.GetCommentBuilder());
-
-  if (!ClassDefinition::WriteJavaFile(&r_class, out_package_name, options_.use_final, out)) {
-    return false;
-  }
-
-  out->flush();
-
-  if (out_r_txt != nullptr) {
-    out_r_txt->flush();
-
-    if (!*out_r_txt) {
-      error_ = android::base::SystemErrorCodeToString(errno);
-      return false;
-    }
-  }
-
+  ClassDefinition::WriteJavaFile(&r_class, out_package_name, options_.use_final, out);
   return true;
 }
 
diff --git a/tools/aapt2/java/JavaClassGenerator.h b/tools/aapt2/java/JavaClassGenerator.h
index 2541749..4992f07 100644
--- a/tools/aapt2/java/JavaClassGenerator.h
+++ b/tools/aapt2/java/JavaClassGenerator.h
@@ -17,16 +17,16 @@
 #ifndef AAPT_JAVA_CLASS_GENERATOR_H
 #define AAPT_JAVA_CLASS_GENERATOR_H
 
-#include <ostream>
 #include <string>
 
 #include "androidfw/StringPiece.h"
 
 #include "ResourceTable.h"
 #include "ResourceValues.h"
-#include "androidfw/StringPiece.h"
+#include "io/Io.h"
 #include "process/IResourceTableConsumer.h"
 #include "process/SymbolTable.h"
+#include "text/Printer.h"
 
 namespace aapt {
 
@@ -70,14 +70,14 @@
   // All symbols technically belong to a single package, but linked libraries will
   // have their names mangled, denoting that they came from a different package.
   // We need to generate these symbols in a separate file. Returns true on success.
-  bool Generate(const android::StringPiece& package_name_to_generate, std::ostream* out,
-                std::ostream* out_r_txt = nullptr);
+  bool Generate(const android::StringPiece& package_name_to_generate, io::OutputStream* out,
+                io::OutputStream* out_r_txt = nullptr);
 
   bool Generate(const android::StringPiece& package_name_to_generate,
-                const android::StringPiece& output_package_name, std::ostream* out,
-                std::ostream* out_r_txt = nullptr);
+                const android::StringPiece& output_package_name, io::OutputStream* out,
+                io::OutputStream* out_r_txt = nullptr);
 
-  const std::string& getError() const;
+  const std::string& GetError() const;
 
   static std::string TransformToFieldName(const android::StringPiece& symbol);
 
@@ -94,13 +94,13 @@
   bool ProcessType(const android::StringPiece& package_name_to_generate,
                    const ResourceTablePackage& package, const ResourceTableType& type,
                    ClassDefinition* out_type_class_def, MethodDefinition* out_rewrite_method_def,
-                   std::ostream* out_r_txt);
+                   text::Printer* r_txt_printer);
 
   // Writes a resource to the R.java file, optionally writing out a rewrite rule for its package
   // ID if `out_rewrite_method` is not nullptr.
   void ProcessResource(const ResourceNameRef& name, const ResourceId& id,
                        const ResourceEntry& entry, ClassDefinition* out_class_def,
-                       MethodDefinition* out_rewrite_method, std::ostream* out_r_txt);
+                       MethodDefinition* out_rewrite_method, text::Printer* r_txt_printer);
 
   // Writes a styleable resource to the R.java file, optionally writing out a rewrite rule for
   // its package ID if `out_rewrite_method` is not nullptr.
@@ -109,7 +109,7 @@
                         const Styleable& styleable,
                         const android::StringPiece& package_name_to_generate,
                         ClassDefinition* out_class_def, MethodDefinition* out_rewrite_method,
-                        std::ostream* out_r_txt);
+                        text::Printer* r_txt_printer);
 
   IAaptContext* context_;
   ResourceTable* table_;
@@ -117,7 +117,7 @@
   std::string error_;
 };
 
-inline const std::string& JavaClassGenerator::getError() const {
+inline const std::string& JavaClassGenerator::GetError() const {
   return error_;
 }
 
diff --git a/tools/aapt2/java/JavaClassGenerator_test.cpp b/tools/aapt2/java/JavaClassGenerator_test.cpp
index 668e434..02f4cb1 100644
--- a/tools/aapt2/java/JavaClassGenerator_test.cpp
+++ b/tools/aapt2/java/JavaClassGenerator_test.cpp
@@ -16,12 +16,13 @@
 
 #include "java/JavaClassGenerator.h"
 
-#include <sstream>
 #include <string>
 
+#include "io/StringStream.h"
 #include "test/Test.h"
 #include "util/Util.h"
 
+using ::aapt::io::StringOutputStream;
 using ::android::StringPiece;
 using ::testing::HasSubstr;
 using ::testing::Lt;
@@ -45,7 +46,8 @@
           .Build();
   JavaClassGenerator generator(context.get(), table.get(), {});
 
-  std::stringstream out;
+  std::string result;
+  StringOutputStream out(&result);
   EXPECT_FALSE(generator.Generate("android", &out));
 }
 
@@ -69,10 +71,10 @@
           .Build();
   JavaClassGenerator generator(context.get(), table.get(), {});
 
-  std::stringstream out;
+  std::string output;
+  StringOutputStream out(&output);
   EXPECT_TRUE(generator.Generate("android", &out));
-
-  std::string output = out.str();
+  out.Flush();
 
   EXPECT_THAT(output, HasSubstr("public static final int hey_man=0x01020000;"));
   EXPECT_THAT(output, HasSubstr("public static final int[] hey_dude={"));
@@ -93,10 +95,12 @@
           .SetNameManglerPolicy(NameManglerPolicy{"android"})
           .Build();
   JavaClassGenerator generator(context.get(), table.get(), {});
-  std::stringstream out;
-  ASSERT_TRUE(generator.Generate("android", "com.android.internal", &out));
 
-  std::string output = out.str();
+  std::string output;
+  StringOutputStream out(&output);
+  ASSERT_TRUE(generator.Generate("android", "com.android.internal", &out));
+  out.Flush();
+
   EXPECT_THAT(output, HasSubstr("package com.android.internal;"));
   EXPECT_THAT(output, HasSubstr("public static final int one=0x01020000;"));
   EXPECT_THAT(output, Not(HasSubstr("two")));
@@ -117,10 +121,12 @@
           .SetNameManglerPolicy(NameManglerPolicy{"android"})
           .Build();
   JavaClassGenerator generator(context.get(), table.get(), {});
-  std::stringstream out;
-  ASSERT_TRUE(generator.Generate("android", &out));
 
-  std::string output = out.str();
+  std::string output;
+  StringOutputStream out(&output);
+  ASSERT_TRUE(generator.Generate("android", &out));
+  out.Flush();
+
   EXPECT_THAT(output, HasSubstr("public static final class attr"));
   EXPECT_THAT(output, Not(HasSubstr("public static final class ^attr-private")));
 }
@@ -147,9 +153,11 @@
   options.types = JavaClassGeneratorOptions::SymbolTypes::kPublic;
   {
     JavaClassGenerator generator(context.get(), table.get(), options);
-    std::stringstream out;
+    std::string output;
+    StringOutputStream out(&output);
     ASSERT_TRUE(generator.Generate("android", &out));
-    std::string output = out.str();
+    out.Flush();
+
     EXPECT_THAT(output, HasSubstr("public static final int one=0x01020000;"));
     EXPECT_THAT(output, Not(HasSubstr("two")));
     EXPECT_THAT(output, Not(HasSubstr("three")));
@@ -158,9 +166,11 @@
   options.types = JavaClassGeneratorOptions::SymbolTypes::kPublicPrivate;
   {
     JavaClassGenerator generator(context.get(), table.get(), options);
-    std::stringstream out;
+    std::string output;
+    StringOutputStream out(&output);
     ASSERT_TRUE(generator.Generate("android", &out));
-    std::string output = out.str();
+    out.Flush();
+
     EXPECT_THAT(output, HasSubstr("public static final int one=0x01020000;"));
     EXPECT_THAT(output, HasSubstr("public static final int two=0x01020001;"));
     EXPECT_THAT(output, Not(HasSubstr("three")));
@@ -169,9 +179,11 @@
   options.types = JavaClassGeneratorOptions::SymbolTypes::kAll;
   {
     JavaClassGenerator generator(context.get(), table.get(), options);
-    std::stringstream out;
+    std::string output;
+    StringOutputStream out(&output);
     ASSERT_TRUE(generator.Generate("android", &out));
-    std::string output = out.str();
+    out.Flush();
+
     EXPECT_THAT(output, HasSubstr("public static final int one=0x01020000;"));
     EXPECT_THAT(output, HasSubstr("public static final int two=0x01020001;"));
     EXPECT_THAT(output, HasSubstr("public static final int three=0x01020002;"));
@@ -235,10 +247,11 @@
           .Build();
   JavaClassGenerator generator(context.get(), table.get(), {});
 
-  std::stringstream out;
+  std::string output;
+  StringOutputStream out(&output);
   EXPECT_TRUE(generator.Generate("android", &out));
+  out.Flush();
 
-  std::string output = out.str();
   EXPECT_THAT(output, HasSubstr("int foo_bar="));
   EXPECT_THAT(output, HasSubstr("int foo_com_lib_bar="));
 }
@@ -258,9 +271,11 @@
           .SetNameManglerPolicy(NameManglerPolicy{"android"})
           .Build();
   JavaClassGenerator generator(context.get(), table.get(), {});
-  std::stringstream out;
+
+  std::string output;
+  StringOutputStream out(&output);
   ASSERT_TRUE(generator.Generate("android", &out));
-  std::string output = out.str();
+  out.Flush();
 
   const char* expected_text =
       R"EOF(/**
@@ -298,9 +313,11 @@
   JavaClassGeneratorOptions options;
   options.use_final = false;
   JavaClassGenerator generator(context.get(), table.get(), options);
-  std::stringstream out;
+
+  std::string output;
+  StringOutputStream out(&output);
   ASSERT_TRUE(generator.Generate("android", &out));
-  std::string output = out.str();
+  out.Flush();
 
   EXPECT_THAT(output, HasSubstr("attr name android:one"));
   EXPECT_THAT(output, HasSubstr("attr description"));
@@ -332,9 +349,11 @@
 
   JavaClassGeneratorOptions options;
   JavaClassGenerator generator(context.get(), table.get(), {});
-  std::stringstream out;
+
+  std::string output;
+  StringOutputStream out(&output);
   ASSERT_TRUE(generator.Generate("android", &out));
-  std::string output = out.str();
+  out.Flush();
 
   std::string::size_type actionbar_pos = output.find("int[] ActionBar");
   ASSERT_THAT(actionbar_pos, Ne(std::string::npos));
@@ -373,9 +392,11 @@
   JavaClassGeneratorOptions options;
   options.use_final = false;
   JavaClassGenerator generator(context.get(), table.get(), options);
-  std::stringstream out;
+
+  std::string output;
+  StringOutputStream out(&output);
   ASSERT_TRUE(generator.Generate("android", &out));
-  std::string output = out.str();
+  out.Flush();
 
   EXPECT_THAT(output, Not(HasSubstr("@attr name android:one")));
   EXPECT_THAT(output, Not(HasSubstr("@attr description")));
@@ -409,10 +430,10 @@
   options.rewrite_callback_options = OnResourcesLoadedCallbackOptions{{"com.foo", "com.boo"}};
   JavaClassGenerator generator(context.get(), table.get(), options);
 
-  std::stringstream out;
+  std::string output;
+  StringOutputStream out(&output);
   ASSERT_TRUE(generator.Generate("android", &out));
-
-  std::string output = out.str();
+  out.Flush();
 
   EXPECT_THAT(output, HasSubstr("void onResourcesLoaded"));
   EXPECT_THAT(output, HasSubstr("com.foo.R.onResourcesLoaded"));
diff --git a/tools/aapt2/java/ManifestClassGenerator.h b/tools/aapt2/java/ManifestClassGenerator.h
index b12202a..3f6645f 100644
--- a/tools/aapt2/java/ManifestClassGenerator.h
+++ b/tools/aapt2/java/ManifestClassGenerator.h
@@ -23,8 +23,7 @@
 
 namespace aapt {
 
-std::unique_ptr<ClassDefinition> GenerateManifestClass(IDiagnostics* diag,
-                                                       xml::XmlResource* res);
+std::unique_ptr<ClassDefinition> GenerateManifestClass(IDiagnostics* diag, xml::XmlResource* res);
 
 }  // namespace aapt
 
diff --git a/tools/aapt2/java/ManifestClassGenerator_test.cpp b/tools/aapt2/java/ManifestClassGenerator_test.cpp
index ada5634..c324238 100644
--- a/tools/aapt2/java/ManifestClassGenerator_test.cpp
+++ b/tools/aapt2/java/ManifestClassGenerator_test.cpp
@@ -16,8 +16,10 @@
 
 #include "java/ManifestClassGenerator.h"
 
+#include "io/StringStream.h"
 #include "test/Test.h"
 
+using ::aapt::io::StringOutputStream;
 using ::testing::HasSubstr;
 using ::testing::Not;
 
@@ -144,12 +146,9 @@
     return ::testing::AssertionFailure() << "manifest_class == nullptr";
   }
 
-  std::stringstream out;
-  if (!manifest_class->WriteJavaFile(manifest_class.get(), "android", true, &out)) {
-    return ::testing::AssertionFailure() << "failed to write java file";
-  }
-
-  *out_str = out.str();
+  StringOutputStream out(out_str);
+  manifest_class->WriteJavaFile(manifest_class.get(), "android", true, &out);
+  out.Flush();
   return ::testing::AssertionSuccess();
 }
 
diff --git a/tools/aapt2/java/ProguardRules.cpp b/tools/aapt2/java/ProguardRules.cpp
index b214d21..132b234 100644
--- a/tools/aapt2/java/ProguardRules.cpp
+++ b/tools/aapt2/java/ProguardRules.cpp
@@ -20,14 +20,18 @@
 #include <string>
 
 #include "android-base/macros.h"
+#include "androidfw/StringPiece.h"
 
 #include "JavaClassGenerator.h"
 #include "ResourceUtils.h"
 #include "ValueVisitor.h"
-#include "androidfw/StringPiece.h"
+#include "text/Printer.h"
 #include "util/Util.h"
 #include "xml/XmlDom.h"
 
+using ::aapt::io::OutputStream;
+using ::aapt::text::Printer;
+
 namespace aapt {
 namespace proguard {
 
@@ -326,12 +330,13 @@
   return true;
 }
 
-bool WriteKeepSet(std::ostream* out, const KeepSet& keep_set) {
+void WriteKeepSet(const KeepSet& keep_set, OutputStream* out) {
+  Printer printer(out);
   for (const auto& entry : keep_set.manifest_class_set_) {
     for (const UsageLocation& location : entry.second) {
-      *out << "# Referenced at " << location.source << "\n";
+      printer.Print("# Referenced at ").Println(location.source.to_string());
     }
-    *out << "-keep class " << entry.first << " { <init>(...); }\n" << std::endl;
+    printer.Print("-keep class ").Print(entry.first).Println(" { <init>(...); }");
   }
 
   for (const auto& entry : keep_set.conditional_class_set_) {
@@ -342,26 +347,31 @@
     }
 
     for (const UsageLocation& location : entry.second) {
-      *out << "# Referenced at " << location.source << "\n";
+      printer.Print("# Referenced at ").Println(location.source.to_string());
     }
     if (keep_set.conditional_keep_rules_ && can_be_conditional) {
-      *out << "-if class **.R$layout {\n";
+      printer.Println("-if class **.R$layout {");
+      printer.Indent();
       for (const UsageLocation& location : locations) {
-        auto transformed_name = JavaClassGenerator::TransformToFieldName(location.name.entry);
-        *out << "  int " << transformed_name << ";\n";
+        printer.Print("int ")
+            .Print(JavaClassGenerator::TransformToFieldName(location.name.entry))
+            .Println(";");
       }
-      *out << "}\n";
+      printer.Undent();
+      printer.Println("}");
+      printer.Println();
     }
-    *out << "-keep class " << entry.first << " { <init>(...); }\n" << std::endl;
+    printer.Print("-keep class ").Print(entry.first).Println(" { <init>(...); }");
+    printer.Println();
   }
 
   for (const auto& entry : keep_set.method_set_) {
     for (const UsageLocation& location : entry.second) {
-      *out << "# Referenced at " << location.source << "\n";
+      printer.Print("# Referenced at ").Println(location.source.to_string());
     }
-    *out << "-keepclassmembers class * { *** " << entry.first << "(...); }\n" << std::endl;
+    printer.Print("-keepclassmembers class * { *** ").Print(entry.first).Println("(...); }");
+    printer.Println();
   }
-  return true;
 }
 
 bool CollectLocations(const UsageLocation& location, const KeepSet& keep_set,
diff --git a/tools/aapt2/java/ProguardRules.h b/tools/aapt2/java/ProguardRules.h
index 8dbe3c2..46827ee 100644
--- a/tools/aapt2/java/ProguardRules.h
+++ b/tools/aapt2/java/ProguardRules.h
@@ -22,11 +22,13 @@
 #include <set>
 #include <string>
 
+#include "androidfw/StringPiece.h"
+
 #include "Resource.h"
 #include "ResourceTable.h"
 #include "Source.h"
 #include "ValueVisitor.h"
-#include "androidfw/StringPiece.h"
+#include "io/Io.h"
 #include "process/IResourceTableConsumer.h"
 #include "xml/XmlDom.h"
 
@@ -62,7 +64,7 @@
   }
 
  private:
-  friend bool WriteKeepSet(std::ostream* out, const KeepSet& keep_set);
+  friend void WriteKeepSet(const KeepSet& keep_set, io::OutputStream* out);
 
   friend bool CollectLocations(const UsageLocation& location, const KeepSet& keep_set,
                                std::set<UsageLocation>* locations);
@@ -76,11 +78,12 @@
 
 bool CollectProguardRulesForManifest(xml::XmlResource* res, KeepSet* keep_set,
                                      bool main_dex_only = false);
-bool CollectProguardRules(xml::XmlResource* res, KeepSet* keep_set);
-bool CollectResourceReferences(aapt::IAaptContext* context, ResourceTable* table,
-                               KeepSet* keep_set);
 
-bool WriteKeepSet(std::ostream* out, const KeepSet& keep_set);
+bool CollectProguardRules(xml::XmlResource* res, KeepSet* keep_set);
+
+bool CollectResourceReferences(IAaptContext* context, ResourceTable* table, KeepSet* keep_set);
+
+void WriteKeepSet(const KeepSet& keep_set, io::OutputStream* out);
 
 bool CollectLocations(const UsageLocation& location, const KeepSet& keep_set,
                       std::set<UsageLocation>* locations);
diff --git a/tools/aapt2/java/ProguardRules_test.cpp b/tools/aapt2/java/ProguardRules_test.cpp
index 802c56a..37d1a5f 100644
--- a/tools/aapt2/java/ProguardRules_test.cpp
+++ b/tools/aapt2/java/ProguardRules_test.cpp
@@ -17,13 +17,23 @@
 #include "java/ProguardRules.h"
 #include "link/Linkers.h"
 
+#include "io/StringStream.h"
 #include "test/Test.h"
 
+using ::aapt::io::StringOutputStream;
 using ::testing::HasSubstr;
 using ::testing::Not;
 
 namespace aapt {
 
+std::string GetKeepSetString(const proguard::KeepSet& set) {
+  std::string out;
+  StringOutputStream sout(&out);
+  proguard::WriteKeepSet(set, &sout);
+  sout.Flush();
+  return out;
+}
+
 TEST(ProguardRulesTest, FragmentNameRuleIsEmitted) {
   std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
   std::unique_ptr<xml::XmlResource> layout = test::BuildXmlDom(R"(
@@ -34,10 +44,8 @@
   proguard::KeepSet set;
   ASSERT_TRUE(proguard::CollectProguardRules(layout.get(), &set));
 
-  std::stringstream out;
-  ASSERT_TRUE(proguard::WriteKeepSet(&out, set));
+  std::string actual = GetKeepSetString(set);
 
-  std::string actual = out.str();
   EXPECT_THAT(actual, HasSubstr("com.foo.Bar"));
 }
 
@@ -50,10 +58,8 @@
   proguard::KeepSet set;
   ASSERT_TRUE(proguard::CollectProguardRules(layout.get(), &set));
 
-  std::stringstream out;
-  ASSERT_TRUE(proguard::WriteKeepSet(&out, set));
+  std::string actual = GetKeepSetString(set);
 
-  std::string actual = out.str();
   EXPECT_THAT(actual, HasSubstr("com.foo.Bar"));
 }
 
@@ -68,10 +74,8 @@
   proguard::KeepSet set;
   ASSERT_TRUE(proguard::CollectProguardRules(layout.get(), &set));
 
-  std::stringstream out;
-  ASSERT_TRUE(proguard::WriteKeepSet(&out, set));
+  std::string actual = GetKeepSetString(set);
 
-  std::string actual = out.str();
   EXPECT_THAT(actual, HasSubstr("com.foo.Bar"));
   EXPECT_THAT(actual, HasSubstr("com.foo.Baz"));
 }
@@ -87,10 +91,8 @@
   proguard::KeepSet set;
   ASSERT_TRUE(proguard::CollectProguardRules(layout.get(), &set));
 
-  std::stringstream out;
-  ASSERT_TRUE(proguard::WriteKeepSet(&out, set));
+  std::string actual = GetKeepSetString(set);
 
-  std::string actual = out.str();
   EXPECT_THAT(actual, HasSubstr("com.foo.Bar"));
 }
 
@@ -126,11 +128,10 @@
   ASSERT_TRUE(proguard::CollectProguardRules(bar_layout.get(), &set));
   ASSERT_TRUE(proguard::CollectProguardRules(foo_layout.get(), &set));
 
-  std::stringstream out;
-  ASSERT_TRUE(proguard::WriteKeepSet(&out, set));
+  std::string actual = GetKeepSetString(set);
 
-  std::string actual = out.str();
   EXPECT_THAT(actual, HasSubstr("-if class **.R$layout"));
+  EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar { <init>(...); }"));
   EXPECT_THAT(actual, HasSubstr("int foo"));
   EXPECT_THAT(actual, HasSubstr("int bar"));
   EXPECT_THAT(actual, HasSubstr("com.foo.Bar"));
@@ -148,10 +149,9 @@
   set.AddReference({test::ParseNameOrDie("layout/bar"), {}}, layout->file.name);
   ASSERT_TRUE(proguard::CollectProguardRules(layout.get(), &set));
 
-  std::stringstream out;
-  ASSERT_TRUE(proguard::WriteKeepSet(&out, set));
+  std::string actual = GetKeepSetString(set);
 
-  std::string actual = out.str();
+  EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar { <init>(...); }"));
   EXPECT_THAT(actual, HasSubstr("-if class **.R$layout"));
   EXPECT_THAT(actual, HasSubstr("int foo"));
   EXPECT_THAT(actual, HasSubstr("int bar"));
@@ -170,11 +170,10 @@
   set.AddReference({test::ParseNameOrDie("style/MyStyle"), {}}, layout->file.name);
   ASSERT_TRUE(proguard::CollectProguardRules(layout.get(), &set));
 
-  std::stringstream out;
-  ASSERT_TRUE(proguard::WriteKeepSet(&out, set));
+  std::string actual = GetKeepSetString(set);
 
-  std::string actual = out.str();
   EXPECT_THAT(actual, Not(HasSubstr("-if")));
+  EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar { <init>(...); }"));
 }
 
 TEST(ProguardRulesTest, ViewOnClickRuleIsEmitted) {
@@ -187,10 +186,8 @@
   proguard::KeepSet set;
   ASSERT_TRUE(proguard::CollectProguardRules(layout.get(), &set));
 
-  std::stringstream out;
-  ASSERT_TRUE(proguard::WriteKeepSet(&out, set));
+  std::string actual = GetKeepSetString(set);
 
-  std::string actual = out.str();
   EXPECT_THAT(actual, HasSubstr("bar_method"));
 }
 
@@ -208,10 +205,8 @@
   proguard::KeepSet set;
   ASSERT_TRUE(proguard::CollectProguardRules(menu.get(), &set));
 
-  std::stringstream out;
-  ASSERT_TRUE(proguard::WriteKeepSet(&out, set));
+  std::string actual = GetKeepSetString(set);
 
-  std::string actual = out.str();
   EXPECT_THAT(actual, HasSubstr("on_click"));
   EXPECT_THAT(actual, HasSubstr("com.foo.Bar"));
   EXPECT_THAT(actual, HasSubstr("com.foo.Baz"));
diff --git a/tools/aapt2/text/Printer.cpp b/tools/aapt2/text/Printer.cpp
index 38b3585..243800c 100644
--- a/tools/aapt2/text/Printer.cpp
+++ b/tools/aapt2/text/Printer.cpp
@@ -26,18 +26,18 @@
 namespace aapt {
 namespace text {
 
-void Printer::Println(const StringPiece& str) {
+Printer& Printer::Println(const StringPiece& str) {
   Print(str);
-  Print("\n");
+  return Print("\n");
 }
 
-void Printer::Println() {
-  Print("\n");
+Printer& Printer::Println() {
+  return Print("\n");
 }
 
-void Printer::Print(const StringPiece& str) {
+Printer& Printer::Print(const StringPiece& str) {
   if (error_) {
-    return;
+    return *this;
   }
 
   auto remaining_str_begin = str.begin();
@@ -53,7 +53,7 @@
         for (int i = 0; i < indent_level_; i++) {
           if (!io::Copy(out_, "  ")) {
             error_ = true;
-            return;
+            return *this;
           }
         }
         needs_indent_ = false;
@@ -61,7 +61,7 @@
 
       if (!io::Copy(out_, str_to_copy)) {
         error_ = true;
-        return;
+        return *this;
       }
     }
 
@@ -69,7 +69,7 @@
     if (new_line_iter != remaining_str_end) {
       if (!io::Copy(out_, "\n")) {
         error_ = true;
-        return;
+        return *this;
       }
       needs_indent_ = true;
       // Ok to increment iterator here because we know that the '\n' character is one byte.
@@ -78,6 +78,7 @@
       remaining_str_begin = new_line_iter;
     }
   }
+  return *this;
 }
 
 void Printer::Indent() {
diff --git a/tools/aapt2/text/Printer.h b/tools/aapt2/text/Printer.h
index 94b3c0b..f399f8e 100644
--- a/tools/aapt2/text/Printer.h
+++ b/tools/aapt2/text/Printer.h
@@ -31,9 +31,9 @@
   explicit Printer(::aapt::io::OutputStream* out) : out_(out) {
   }
 
-  void Print(const ::android::StringPiece& str);
-  void Println(const ::android::StringPiece& str);
-  void Println();
+  Printer& Print(const ::android::StringPiece& str);
+  Printer& Println(const ::android::StringPiece& str);
+  Printer& Println();
 
   void Indent();
   void Undent();