Add wrapping support to IndentingPrintWriter.

When created with a wrapping width, any content longer than the
requested width will be wrapped onto additional lines.  This supports
use-cases like dumping lots of data with printPair().

Improve documentation and add tests to verify behavior.

Change-Id: Ibdfce198f0e69f4df7725544fd1cd02fa029c647
diff --git a/core/java/com/android/internal/util/IndentingPrintWriter.java b/core/java/com/android/internal/util/IndentingPrintWriter.java
index dd5918b..d01a817 100644
--- a/core/java/com/android/internal/util/IndentingPrintWriter.java
+++ b/core/java/com/android/internal/util/IndentingPrintWriter.java
@@ -21,29 +21,47 @@
 
 /**
  * Lightweight wrapper around {@link PrintWriter} that automatically indents
- * newlines based on internal state. Delays writing indent until first actual
- * write on a newline, enabling indent modification after newline.
+ * newlines based on internal state. It also automatically wraps long lines
+ * based on given line length.
+ * <p>
+ * Delays writing indent until first actual write on a newline, enabling indent
+ * modification after newline.
  */
 public class IndentingPrintWriter extends PrintWriter {
-    private final String mIndent;
+    private final String mSingleIndent;
+    private final int mWrapLength;
 
-    private StringBuilder mBuilder = new StringBuilder();
-    private char[] mCurrent;
+    /** Mutable version of current indent */
+    private StringBuilder mIndentBuilder = new StringBuilder();
+    /** Cache of current {@link #mIndentBuilder} value */
+    private char[] mCurrentIndent;
+    /** Length of current line being built, excluding any indent */
+    private int mCurrentLength;
+
+    /**
+     * Flag indicating if we're currently sitting on an empty line, and that
+     * next write should be prefixed with the current indent.
+     */
     private boolean mEmptyLine = true;
 
-    public IndentingPrintWriter(Writer writer, String indent) {
+    public IndentingPrintWriter(Writer writer, String singleIndent) {
+        this(writer, singleIndent, -1);
+    }
+
+    public IndentingPrintWriter(Writer writer, String singleIndent, int wrapLength) {
         super(writer);
-        mIndent = indent;
+        mSingleIndent = singleIndent;
+        mWrapLength = wrapLength;
     }
 
     public void increaseIndent() {
-        mBuilder.append(mIndent);
-        mCurrent = null;
+        mIndentBuilder.append(mSingleIndent);
+        mCurrentIndent = null;
     }
 
     public void decreaseIndent() {
-        mBuilder.delete(0, mIndent.length());
-        mCurrent = null;
+        mIndentBuilder.delete(0, mSingleIndent.length());
+        mCurrentIndent = null;
     }
 
     public void printPair(String key, Object value) {
@@ -52,33 +70,56 @@
 
     @Override
     public void write(char[] buf, int offset, int count) {
+        final int indentLength = mIndentBuilder.length();
         final int bufferEnd = offset + count;
         int lineStart = offset;
         int lineEnd = offset;
+
+        // March through incoming buffer looking for newlines
         while (lineEnd < bufferEnd) {
             char ch = buf[lineEnd++];
+            mCurrentLength++;
             if (ch == '\n') {
-                writeIndent();
+                maybeWriteIndent();
                 super.write(buf, lineStart, lineEnd - lineStart);
                 lineStart = lineEnd;
                 mEmptyLine = true;
+                mCurrentLength = 0;
+            }
+
+            // Wrap if we've pushed beyond line length
+            if (mWrapLength > 0 && mCurrentLength >= mWrapLength - indentLength) {
+                if (!mEmptyLine) {
+                    // Give ourselves a fresh line to work with
+                    super.write('\n');
+                    mEmptyLine = true;
+                    mCurrentLength = lineEnd - lineStart;
+                } else {
+                    // We need more than a dedicated line, slice it hard
+                    maybeWriteIndent();
+                    super.write(buf, lineStart, lineEnd - lineStart);
+                    super.write('\n');
+                    mEmptyLine = true;
+                    lineStart = lineEnd;
+                    mCurrentLength = 0;
+                }
             }
         }
 
         if (lineStart != lineEnd) {
-            writeIndent();
+            maybeWriteIndent();
             super.write(buf, lineStart, lineEnd - lineStart);
         }
     }
 
-    private void writeIndent() {
+    private void maybeWriteIndent() {
         if (mEmptyLine) {
             mEmptyLine = false;
-            if (mBuilder.length() != 0) {
-                if (mCurrent == null) {
-                    mCurrent = mBuilder.toString().toCharArray();
+            if (mIndentBuilder.length() != 0) {
+                if (mCurrentIndent == null) {
+                    mCurrentIndent = mIndentBuilder.toString().toCharArray();
                 }
-                super.write(mCurrent, 0, mCurrent.length);
+                super.write(mCurrentIndent, 0, mCurrentIndent.length);
             }
         }
     }
diff --git a/core/tests/coretests/src/com/android/internal/util/IndentingPrintWriterTest.java b/core/tests/coretests/src/com/android/internal/util/IndentingPrintWriterTest.java
new file mode 100644
index 0000000..6773612
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/util/IndentingPrintWriterTest.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2013 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.
+ */
+
+package com.android.internal.util;
+
+import junit.framework.TestCase;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintWriter;
+
+/**
+ * Tests for {@link IndentingPrintWriter}.
+ */
+public class IndentingPrintWriterTest extends TestCase {
+
+    private ByteArrayOutputStream mStream;
+    private PrintWriter mWriter;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        mStream = new ByteArrayOutputStream();
+        mWriter = new PrintWriter(mStream);
+    }
+
+    public void testMultipleIndents() throws Exception {
+        final IndentingPrintWriter pw = new IndentingPrintWriter(mWriter, "  ");
+
+        pw.print("Hello");
+        pw.increaseIndent();
+        pw.println();
+        pw.print("World");
+        pw.increaseIndent();
+        pw.println();
+        pw.print("And");
+        pw.decreaseIndent();
+        pw.println();
+        pw.print("Goodbye");
+        pw.decreaseIndent();
+        pw.println();
+        pw.print("World");
+        pw.println();
+
+        pw.flush();
+        assertEquals("Hello\n  World\n    And\n  Goodbye\nWorld\n", mStream.toString());
+    }
+
+    public void testAdjustIndentAfterNewline() throws Exception {
+        final IndentingPrintWriter pw = new IndentingPrintWriter(mWriter, "  ");
+
+        pw.println("Hello");
+        pw.increaseIndent();
+        pw.println("World");
+
+        pw.flush();
+        assertEquals("Hello\n  World\n", mStream.toString());
+    }
+
+    public void testWrapping() throws Exception {
+        final IndentingPrintWriter pw = new IndentingPrintWriter(mWriter, "", 10);
+
+        pw.print("dog ");
+        pw.print("cat ");
+        pw.print("cow ");
+        pw.print("meow ");
+
+        pw.flush();
+        assertEquals("dog cat \ncow meow ", mStream.toString());
+    }
+
+    public void testWrappingIndented() throws Exception {
+        final IndentingPrintWriter pw = new IndentingPrintWriter(mWriter, "    ", 10);
+
+        pw.increaseIndent();
+        pw.print("dog ");
+        pw.print("meow ");
+        pw.print("a ");
+        pw.print("b ");
+        pw.print("cow ");
+
+        pw.flush();
+        assertEquals("    dog \n    meow \n    a b \n    cow ", mStream.toString());
+    }
+
+    public void testWrappingEmbeddedNewlines() throws Exception {
+        final IndentingPrintWriter pw = new IndentingPrintWriter(mWriter, "  ", 10);
+
+        pw.increaseIndent();
+        pw.print("Lorem ipsum \ndolor sit \namet, consectetur \nadipiscing elit.");
+
+        pw.flush();
+        assertEquals("  Lorem ip\n  sum \n  dolor si\n  t \n  amet, co\n"
+                + "  nsectetu\n  r \n  adipisci\n  ng elit.\n", mStream.toString());
+    }
+
+    public void testWrappingSingleGiant() throws Exception {
+        final IndentingPrintWriter pw = new IndentingPrintWriter(mWriter, "  ", 10);
+
+        pw.increaseIndent();
+        pw.print("Lorem ipsum dolor sit amet, consectetur adipiscing elit.");
+
+        pw.flush();
+        assertEquals("  Lorem ip\n  sum dolo\n  r sit am\n  et, cons\n"
+                + "  ectetur \n  adipisci\n  ng elit.\n", mStream.toString());
+    }
+
+    public void testWrappingPrefixedGiant() throws Exception {
+        final IndentingPrintWriter pw = new IndentingPrintWriter(mWriter, "  ", 10);
+
+        pw.increaseIndent();
+        pw.print("foo");
+        pw.print("Lorem ipsum dolor sit amet, consectetur adipiscing elit.");
+
+        pw.flush();
+        assertEquals("  foo\n  Lorem ip\n  sum dolo\n  r sit am\n  et, cons\n"
+                + "  ectetur \n  adipisci\n  ng elit.\n", mStream.toString());
+    }
+}