Always redraw text that protrude from line bounds.

With I63af3a6ecbf92, we create RenderNode lazily, but
blocks containing contents that protrude from line top or
bottom cannot be simply lazily redrawn after edit or
scroll.
With this CL, we check if the contents protrude from line
top or bottom by comparing the text bounds with relevant
font metrics values and we always redrawn such blocks after
edit or scroll.

Bug: 27889485
Change-Id: I666da5eeb39f780c341597f347bfcba21eb34295
diff --git a/apct-tests/perftests/core/src/android/text/DynamicLayoutPerfTest.java b/apct-tests/perftests/core/src/android/text/DynamicLayoutPerfTest.java
new file mode 100644
index 0000000..e644a1f
--- /dev/null
+++ b/apct-tests/perftests/core/src/android/text/DynamicLayoutPerfTest.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2016 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 android.text;
+
+import android.app.Activity;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Paint.FontMetricsInt;
+import android.os.Bundle;
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+import android.perftests.utils.StubActivity;
+
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.text.style.ReplacementSpan;
+import android.util.ArraySet;
+
+import static android.text.Layout.Alignment.ALIGN_NORMAL;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Locale;
+import java.util.Random;
+
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized.Parameters;
+import org.junit.runners.Parameterized;
+
+@LargeTest
+@RunWith(Parameterized.class)
+public class DynamicLayoutPerfTest {
+
+    @Parameters(name = "{0}")
+    public static Collection cases() {
+        return Arrays.asList(new Object[][] {
+            { "0%", 0.0f},
+            { "1%", 0.01f},
+            { "5%", 0.05f},
+            { "30%", 0.3f},
+            { "100%", 1.0f},
+        });
+    }
+
+    private final String mMetricKey;
+    private final float mProbability;
+    public DynamicLayoutPerfTest(String metricKey, float probability) {
+        mMetricKey = metricKey;
+        mProbability = probability;
+    }
+
+    private static class MockReplacementSpan extends ReplacementSpan {
+        @Override
+        public int getSize(Paint paint, CharSequence text, int start, int end, FontMetricsInt fm) {
+            return 10;
+        }
+
+        @Override
+        public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top,
+                int y, int bottom, Paint paint) {
+        }
+    }
+
+    @Rule
+    public ActivityTestRule<StubActivity> mActivityRule = new ActivityTestRule(StubActivity.class);
+
+    @Rule
+    public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+
+    private final static String ALPHABETS = "abcdefghijklmnopqrstuvwxyz";
+
+    private SpannableStringBuilder getText() {
+        final long seed = 1234567890;
+        final Random r = new Random(seed);
+        final SpannableStringBuilder builder = new SpannableStringBuilder();
+
+        final int paragraphCount = 100;
+        for (int i = 0; i < paragraphCount; i++) {
+            final int wordCount = 5 + r.nextInt(20);
+            final boolean containsReplacementSpan = r.nextFloat() < mProbability;
+            final int replacedWordIndex = containsReplacementSpan ? r.nextInt(wordCount) : -1;
+            for (int j = 0; j < wordCount; j++) {
+                final int startIndex = builder.length();
+                final int wordLength = 1 + r.nextInt(10);
+                for (int k = 0; k < wordLength; k++) {
+                    char c = ALPHABETS.charAt(r.nextInt(ALPHABETS.length()));
+                    builder.append(c);
+                }
+                if (replacedWordIndex == j) {
+                    builder.setSpan(new MockReplacementSpan(), startIndex,
+                            builder.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+                }
+                builder.append(' ');
+            }
+            builder.append('\n');
+        }
+        return builder;
+    }
+
+    @Test
+    public void testGetBlocksAlwaysNeedToBeRedrawn() {
+        final SpannableStringBuilder text = getText();
+        final DynamicLayout layout = new DynamicLayout(text, new TextPaint(), 1000,
+                ALIGN_NORMAL, 0, 0, false);
+
+        BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+        final int steps = 10;
+        while (state.keepRunning()) {
+            for (int i = 0; i < steps; i++) {
+                int offset = (text.length() * i) / steps;
+                text.insert(offset, "\n");
+                text.delete(offset, offset + 1);
+                final ArraySet<Integer> set = layout.getBlocksAlwaysNeedToBeRedrawn();
+                if (set != null) {
+                    for (int j = 0; j < set.size(); j++) {
+                        layout.getBlockIndex(set.valueAt(j));
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/core/java/android/text/DynamicLayout.java b/core/java/android/text/DynamicLayout.java
index 3c7741d..9ac8996 100644
--- a/core/java/android/text/DynamicLayout.java
+++ b/core/java/android/text/DynamicLayout.java
@@ -17,8 +17,11 @@
 package android.text;
 
 import android.graphics.Paint;
+import android.graphics.Rect;
+import android.text.style.ReplacementSpan;
 import android.text.style.UpdateLayout;
 import android.text.style.WrapTogetherSpan;
+import android.util.ArraySet;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ArrayUtils;
@@ -300,7 +303,6 @@
                 .setHyphenationFrequency(mHyphenationFrequency);
         reflowed.generate(b, false, true);
         int n = reflowed.getLineCount();
-
         // If the new layout has a blank line at the end, but it is not
         // the very end of the buffer, then we already have a line that
         // starts there, so disregard the blank line.
@@ -345,9 +347,10 @@
         Directions[] objects = new Directions[1];
 
         for (int i = 0; i < n; i++) {
-            ints[START] = reflowed.getLineStart(i) |
-                          (reflowed.getParagraphDirection(i) << DIR_SHIFT) |
-                          (reflowed.getLineContainsTab(i) ? TAB_MASK : 0);
+            final int start = reflowed.getLineStart(i);
+            ints[START] = start;
+            ints[DIR] |= reflowed.getParagraphDirection(i) << DIR_SHIFT;
+            ints[TAB] |= reflowed.getLineContainsTab(i) ? TAB_MASK : 0;
 
             int top = reflowed.getLineTop(i) + startv;
             if (i > 0)
@@ -361,7 +364,11 @@
             ints[DESCENT] = desc;
             objects[0] = reflowed.getLineDirections(i);
 
-            ints[HYPHEN] = reflowed.getHyphen(i);
+            final int end = (i == n - 1) ? where + after : reflowed.getLineStart(i + 1);
+            ints[HYPHEN] = reflowed.getHyphen(i) & HYPHEN_MASK;
+            ints[MAY_PROTRUDE_FROM_TOP_OR_BOTTOM] |=
+                    contentMayProtrudeFromLineTopOrBottom(text, start, end) ?
+                            MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK : 0;
 
             if (mEllipsize) {
                 ints[ELLIPSIS_START] = reflowed.getEllipsisStart(i);
@@ -381,6 +388,21 @@
         }
     }
 
+    private boolean contentMayProtrudeFromLineTopOrBottom(CharSequence text, int start, int end) {
+        if (text instanceof Spanned) {
+            final Spanned spanned = (Spanned) text;
+            if (spanned.getSpans(start, end, ReplacementSpan.class).length > 0) {
+                return true;
+            }
+        }
+        // Spans other than ReplacementSpan can be ignored because line top and bottom are
+        // disjunction of all tops and bottoms, although it's not optimal.
+        final Paint paint = getPaint();
+        paint.getTextBounds(text, start, end, mTempRect);
+        final Paint.FontMetricsInt fm = paint.getFontMetricsInt();
+        return mTempRect.top < fm.top || mTempRect.bottom > fm.bottom;
+    }
+
     /**
      * Create the initial block structure, cutting the text into blocks of at least
      * BLOCK_MINIMUM_CHARACTER_SIZE characters, aligned on the ends of paragraphs.
@@ -409,17 +431,41 @@
     }
 
     /**
+     * @hide
+     */
+    public ArraySet<Integer> getBlocksAlwaysNeedToBeRedrawn() {
+        return mBlocksAlwaysNeedToBeRedrawn;
+    }
+
+    private void updateAlwaysNeedsToBeRedrawn(int blockIndex) {
+        int startLine = blockIndex == 0 ? 0 : (mBlockEndLines[blockIndex - 1] + 1);
+        int endLine = mBlockEndLines[blockIndex];
+        for (int i = startLine; i <= endLine; i++) {
+            if (getContentMayProtrudeFromTopOrBottom(i)) {
+                if (mBlocksAlwaysNeedToBeRedrawn == null) {
+                    mBlocksAlwaysNeedToBeRedrawn = new ArraySet<>();
+                }
+                mBlocksAlwaysNeedToBeRedrawn.add(blockIndex);
+                return;
+            }
+        }
+        if (mBlocksAlwaysNeedToBeRedrawn != null) {
+            mBlocksAlwaysNeedToBeRedrawn.remove(blockIndex);
+        }
+    }
+
+    /**
      * Create a new block, ending at the specified character offset.
      * A block will actually be created only if has at least one line, i.e. this offset is
      * not on the end line of the previous block.
      */
     private void addBlockAtOffset(int offset) {
         final int line = getLineForOffset(offset);
-
         if (mBlockEndLines == null) {
             // Initial creation of the array, no test on previous block ending line
             mBlockEndLines = ArrayUtils.newUnpaddedIntArray(1);
             mBlockEndLines[mNumberOfBlocks] = line;
+            updateAlwaysNeedsToBeRedrawn(mNumberOfBlocks);
             mNumberOfBlocks++;
             return;
         }
@@ -427,6 +473,7 @@
         final int previousBlockEndLine = mBlockEndLines[mNumberOfBlocks - 1];
         if (line > previousBlockEndLine) {
             mBlockEndLines = GrowingArrayUtils.append(mBlockEndLines, mNumberOfBlocks, line);
+            updateAlwaysNeedsToBeRedrawn(mNumberOfBlocks);
             mNumberOfBlocks++;
         }
     }
@@ -506,13 +553,25 @@
                     blockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
             mBlockEndLines = blockEndLines;
             mBlockIndices = blockIndices;
-        } else {
+        } else if (numAddedBlocks + numRemovedBlocks != 0) {
             System.arraycopy(mBlockEndLines, lastBlock + 1,
                     mBlockEndLines, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
             System.arraycopy(mBlockIndices, lastBlock + 1,
                     mBlockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
         }
 
+        if (numAddedBlocks + numRemovedBlocks != 0 && mBlocksAlwaysNeedToBeRedrawn != null) {
+            final ArraySet<Integer> set = new ArraySet<>();
+            for (int i = 0; i < mBlocksAlwaysNeedToBeRedrawn.size(); i++) {
+                Integer block = mBlocksAlwaysNeedToBeRedrawn.valueAt(i);
+                if (block > firstBlock) {
+                    block += numAddedBlocks - numRemovedBlocks;
+                }
+                set.add(block);
+            }
+            mBlocksAlwaysNeedToBeRedrawn = set;
+        }
+
         mNumberOfBlocks = newNumberOfBlocks;
         int newFirstChangedBlock;
         final int deltaLines = newLineCount - (endLine - startLine + 1);
@@ -531,18 +590,21 @@
         int blockIndex = firstBlock;
         if (createBlockBefore) {
             mBlockEndLines[blockIndex] = startLine - 1;
+            updateAlwaysNeedsToBeRedrawn(blockIndex);
             mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
             blockIndex++;
         }
 
         if (createBlock) {
             mBlockEndLines[blockIndex] = startLine + newLineCount - 1;
+            updateAlwaysNeedsToBeRedrawn(blockIndex);
             mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
             blockIndex++;
         }
 
         if (createBlockAfter) {
             mBlockEndLines[blockIndex] = lastBlockEndLine + deltaLines;
+            updateAlwaysNeedsToBeRedrawn(blockIndex);
             mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
         }
     }
@@ -577,6 +639,21 @@
     /**
      * @hide
      */
+    public int getBlockIndex(int index) {
+        return mBlockIndices[index];
+    }
+
+    /**
+     * @hide
+     * @param index
+     */
+    public void setBlockIndex(int index, int blockIndex) {
+        mBlockIndices[index] = blockIndex;
+    }
+
+    /**
+     * @hide
+     */
     public int getNumberOfBlocks() {
         return mNumberOfBlocks;
     }
@@ -645,7 +722,12 @@
      */
     @Override
     public int getHyphen(int line) {
-        return mInts.getValue(line, HYPHEN);
+        return mInts.getValue(line, HYPHEN) & HYPHEN_MASK;
+    }
+
+    private boolean getContentMayProtrudeFromTopOrBottom(int line) {
+        return (mInts.getValue(line, MAY_PROTRUDE_FROM_TOP_OR_BOTTOM)
+                & MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK) != 0;
     }
 
     @Override
@@ -741,6 +823,8 @@
     // The indices of this block's display list in TextView's internal display list array or
     // INVALID_BLOCK_INDEX if this block has been invalidated during an edition
     private int[] mBlockIndices;
+    // Set of blocks that always need to be redrawn.
+    private ArraySet<Integer> mBlocksAlwaysNeedToBeRedrawn;
     // Number of items actually currently being used in the above 2 arrays
     private int mNumberOfBlocks;
     // The first index of the blocks whose locations are changed
@@ -748,17 +832,22 @@
 
     private int mTopPadding, mBottomPadding;
 
+    private Rect mTempRect = new Rect();
+
     private static StaticLayout sStaticLayout = null;
     private static StaticLayout.Builder sBuilder = null;
 
     private static final Object[] sLock = new Object[0];
 
+    // START, DIR, and TAB share the same entry.
     private static final int START = 0;
     private static final int DIR = START;
     private static final int TAB = START;
     private static final int TOP = 1;
     private static final int DESCENT = 2;
+    // HYPHEN and MAY_PROTRUDE_FROM_TOP_OR_BOTTOM share the same entry.
     private static final int HYPHEN = 3;
+    private static final int MAY_PROTRUDE_FROM_TOP_OR_BOTTOM = HYPHEN;
     private static final int COLUMNS_NORMAL = 4;
 
     private static final int ELLIPSIS_START = 4;
@@ -768,6 +857,8 @@
     private static final int START_MASK = 0x1FFFFFFF;
     private static final int DIR_SHIFT  = 30;
     private static final int TAB_MASK   = 0x20000000;
+    private static final int HYPHEN_MASK = 0xFF;
+    private static final int MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK = 0x100;
 
     private static final int ELLIPSIS_UNDEFINED = 0x80000000;
 }
diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java
index bb131a0..70d183d 100644
--- a/core/java/android/text/StaticLayout.java
+++ b/core/java/android/text/StaticLayout.java
@@ -1181,7 +1181,7 @@
      */
     @Override
     public int getHyphen(int line) {
-        return mLines[mColumns * line + HYPHEN] & 0xff;
+        return mLines[mColumns * line + HYPHEN] & HYPHEN_MASK;
     }
 
     /**
@@ -1295,6 +1295,7 @@
     private static final int START_MASK = 0x1FFFFFFF;
     private static final int DIR_SHIFT  = 30;
     private static final int TAB_MASK   = 0x20000000;
+    private static final int HYPHEN_MASK = 0xFF;
 
     private static final int TAB_INCREMENT = 20; // same as Layout, but that's private
 
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index b30b7f7..bf49048 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -70,6 +70,7 @@
 import android.text.style.SuggestionSpan;
 import android.text.style.TextAppearanceSpan;
 import android.text.style.URLSpan;
+import android.util.ArraySet;
 import android.util.DisplayMetrics;
 import android.util.Log;
 import android.util.SparseArray;
@@ -1696,6 +1697,17 @@
             final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();
             final int indexFirstChangedBlock = dynamicLayout.getIndexFirstChangedBlock();
 
+            final ArraySet<Integer> blockSet = dynamicLayout.getBlocksAlwaysNeedToBeRedrawn();
+            if (blockSet != null) {
+                for (int i = 0; i < blockSet.size(); i++) {
+                    final int blockIndex = dynamicLayout.getBlockIndex(blockSet.valueAt(i));
+                    if (blockIndex != DynamicLayout.INVALID_BLOCK_INDEX
+                            && mTextRenderNodes[blockIndex] != null) {
+                        mTextRenderNodes[blockIndex].needsToBeShifted = true;
+                    }
+                }
+            }
+
             int startBlock = Arrays.binarySearch(blockEndLines, 0, numberOfBlocks, firstLine);
             if (startBlock < 0) {
                 startBlock = -(startBlock + 1);
@@ -1725,6 +1737,20 @@
                     break;
                 }
             }
+            if (blockSet != null) {
+                for (int i = 0; i < blockSet.size(); i++) {
+                    final int block = blockSet.valueAt(i);
+                    final int blockIndex = dynamicLayout.getBlockIndex(block);
+                    if (blockIndex == DynamicLayout.INVALID_BLOCK_INDEX
+                            || mTextRenderNodes[blockIndex] == null
+                            || mTextRenderNodes[blockIndex].needsToBeShifted) {
+                        startIndexToFindAvailableRenderNode = drawHardwareAcceleratedInner(canvas,
+                                layout, highlight, highlightPaint, cursorOffsetVertical,
+                                blockEndLines, blockIndices, block, numberOfBlocks,
+                                startIndexToFindAvailableRenderNode);
+                    }
+                }
+            }
 
             dynamicLayout.setIndexFirstChangedBlock(lastIndex);
         } else {
diff --git a/core/tests/coretests/src/android/text/DynamicLayoutTest.java b/core/tests/coretests/src/android/text/DynamicLayoutTest.java
new file mode 100644
index 0000000..9362ed9
--- /dev/null
+++ b/core/tests/coretests/src/android/text/DynamicLayoutTest.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2016 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 android.text;
+
+import static android.text.Layout.Alignment.ALIGN_NORMAL;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Paint.FontMetricsInt;
+import android.text.style.ReplacementSpan;
+import junit.framework.TestCase;
+
+public class DynamicLayoutTest extends TestCase {
+    private static final int WIDTH = 10000;
+
+    public void testGetBlocksAlwaysNeedToBeRedrawn_en() {
+        final SpannableStringBuilder builder = new SpannableStringBuilder();
+        final DynamicLayout layout = new DynamicLayout(builder, new TextPaint(), WIDTH,
+                ALIGN_NORMAL, 0, 0, false);
+
+        assertNull(layout.getBlocksAlwaysNeedToBeRedrawn());
+
+        builder.append("abcd efg\n");
+        builder.append("hijk lmn\n");
+        assertNull(layout.getBlocksAlwaysNeedToBeRedrawn());
+
+        builder.delete(0, builder.length());
+        assertNull(layout.getBlocksAlwaysNeedToBeRedrawn());
+    }
+
+
+    private static class MockReplacementSpan extends ReplacementSpan {
+        @Override
+        public int getSize(Paint paint, CharSequence text, int start, int end, FontMetricsInt fm) {
+            return 10;
+        }
+
+        @Override
+        public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top,
+                int y, int bottom, Paint paint) {
+        }
+    }
+
+    public void testGetBlocksAlwaysNeedToBeRedrawn_replacementSpan() {
+        final SpannableStringBuilder builder = new SpannableStringBuilder();
+        final DynamicLayout layout = new DynamicLayout(builder, new TextPaint(), WIDTH,
+                ALIGN_NORMAL, 0, 0, false);
+
+        assertNull(layout.getBlocksAlwaysNeedToBeRedrawn());
+
+        builder.append("abcd efg\n");
+        builder.append("hijk lmn\n");
+        assertNull(layout.getBlocksAlwaysNeedToBeRedrawn());
+
+        builder.setSpan(new MockReplacementSpan(), 0, 4, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+        assertNotNull(layout.getBlocksAlwaysNeedToBeRedrawn());
+        assertTrue(layout.getBlocksAlwaysNeedToBeRedrawn().contains(0));
+
+        builder.setSpan(new MockReplacementSpan(), 9, 13, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+        assertTrue(layout.getBlocksAlwaysNeedToBeRedrawn().contains(0));
+        assertTrue(layout.getBlocksAlwaysNeedToBeRedrawn().contains(1));
+
+        builder.delete(9, 13);
+        assertTrue(layout.getBlocksAlwaysNeedToBeRedrawn().contains(0));
+        assertFalse(layout.getBlocksAlwaysNeedToBeRedrawn().contains(1));
+
+        builder.delete(0, 4);
+        assertFalse(layout.getBlocksAlwaysNeedToBeRedrawn().contains(0));
+        assertTrue(layout.getBlocksAlwaysNeedToBeRedrawn().isEmpty());
+    }
+
+    public void testGetBlocksAlwaysNeedToBeRedrawn_thai() {
+        final SpannableStringBuilder builder = new SpannableStringBuilder();
+        final DynamicLayout layout = new DynamicLayout(builder, new TextPaint(), WIDTH,
+                ALIGN_NORMAL, 0, 0, false);
+
+        assertNull(layout.getBlocksAlwaysNeedToBeRedrawn());
+
+        builder.append("\u0E22\u0E34\u0E19\u0E14\u0E35\u0E15\u0E49\u0E2D\u0E19\u0E23\u0E31\u0E1A");
+        builder.append("\u0E2A\u0E39\u0E48");
+        assertNull(layout.getBlocksAlwaysNeedToBeRedrawn());
+
+        builder.append("\u0E48\u0E48\u0E48\u0E48\u0E48");
+        assertNotNull(layout.getBlocksAlwaysNeedToBeRedrawn());
+        assertTrue(layout.getBlocksAlwaysNeedToBeRedrawn().contains(0));
+
+        builder.delete(builder.length() -5, builder.length());
+        assertFalse(layout.getBlocksAlwaysNeedToBeRedrawn().contains(0));
+        assertTrue(layout.getBlocksAlwaysNeedToBeRedrawn().isEmpty());
+    }
+}
diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java
index 3cbc4f0..81bbfa9 100644
--- a/graphics/java/android/graphics/Paint.java
+++ b/graphics/java/android/graphics/Paint.java
@@ -2397,13 +2397,12 @@
      * Note: just like Canvas.drawText, this will respect the Align setting in
      * the paint.
      *
-     * @param text     The text to retrieve the path from
-     * @param index    The index of the first character in text
-     * @param count    The number of characterss starting with index
-     * @param x        The x coordinate of the text's origin
-     * @param y        The y coordinate of the text's origin
-     * @param path     The path to receive the data describing the text. Must
-     *                 be allocated by the caller.
+     * @param text the text to retrieve the path from
+     * @param index the index of the first character in text
+     * @param count the number of characters starting with index
+     * @param x the x coordinate of the text's origin
+     * @param y the y coordinate of the text's origin
+     * @param path the path to receive the data describing the text. Must be allocated by the caller
      */
     public void getTextPath(char[] text, int index, int count,
                             float x, float y, Path path) {
@@ -2419,13 +2418,12 @@
      * Note: just like Canvas.drawText, this will respect the Align setting
      * in the paint.
      *
-     * @param text  The text to retrieve the path from
-     * @param start The first character in the text
-     * @param end   1 past the last charcter in the text
-     * @param x     The x coordinate of the text's origin
-     * @param y     The y coordinate of the text's origin
-     * @param path  The path to receive the data describing the text. Must
-     *              be allocated by the caller.
+     * @param text the text to retrieve the path from
+     * @param start the first character in the text
+     * @param end 1 past the last character in the text
+     * @param x the x coordinate of the text's origin
+     * @param y the y coordinate of the text's origin
+     * @param path the path to receive the data describing the text. Must be allocated by the caller
      */
     public void getTextPath(String text, int start, int end,
                             float x, float y, Path path) {
@@ -2440,11 +2438,10 @@
      * Return in bounds (allocated by the caller) the smallest rectangle that
      * encloses all of the characters, with an implied origin at (0,0).
      *
-     * @param text  String to measure and return its bounds
-     * @param start Index of the first char in the string to measure
-     * @param end   1 past the last char in the string measure
-     * @param bounds Returns the unioned bounds of all the text. Must be
-     *               allocated by the caller.
+     * @param text string to measure and return its bounds
+     * @param start index of the first char in the string to measure
+     * @param end 1 past the last char in the string to measure
+     * @param bounds returns the unioned bounds of all the text. Must be allocated by the caller
      */
     public void getTextBounds(String text, int start, int end, Rect bounds) {
         if ((start | end | (end - start) | (text.length() - end)) < 0) {
@@ -2460,11 +2457,33 @@
      * Return in bounds (allocated by the caller) the smallest rectangle that
      * encloses all of the characters, with an implied origin at (0,0).
      *
-     * @param text  Array of chars to measure and return their unioned bounds
-     * @param index Index of the first char in the array to measure
-     * @param count The number of chars, beginning at index, to measure
-     * @param bounds Returns the unioned bounds of all the text. Must be
-     *               allocated by the caller.
+     * @param text text to measure and return its bounds
+     * @param start index of the first char in the text to measure
+     * @param end 1 past the last char in the text to measure
+     * @param bounds returns the unioned bounds of all the text. Must be allocated by the caller
+     * @hide
+     */
+    public void getTextBounds(CharSequence text, int start, int end, Rect bounds) {
+        if ((start | end | (end - start) | (text.length() - end)) < 0) {
+            throw new IndexOutOfBoundsException();
+        }
+        if (bounds == null) {
+            throw new NullPointerException("need bounds Rect");
+        }
+        char[] buf = TemporaryBuffer.obtain(end - start);
+        TextUtils.getChars(text, start, end, buf, 0);
+        getTextBounds(buf, 0, end - start, bounds);
+        TemporaryBuffer.recycle(buf);
+    }
+
+    /**
+     * Return in bounds (allocated by the caller) the smallest rectangle that
+     * encloses all of the characters, with an implied origin at (0,0).
+     *
+     * @param text  array of chars to measure and return their unioned bounds
+     * @param index index of the first char in the array to measure
+     * @param count the number of chars, beginning at index, to measure
+     * @param bounds returns the unioned bounds of all the text. Must be allocated by the caller
      */
     public void getTextBounds(char[] text, int index, int count, Rect bounds) {
         if ((index | count) < 0 || index + count > text.length) {