Merge changes from topic 'ksk_long_text_edit'

* changes:
  Always redraw text that protrude from line bounds.
  Lazy RenderNode creation to improve performance.
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 a0447a6..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;
@@ -176,11 +177,21 @@
     InputMethodState mInputMethodState;
 
     private static class TextRenderNode {
+        // Render node has 3 recording states:
+        // 1. Recorded operations are valid.
+        // #needsRecord() returns false, but needsToBeShifted is false.
+        // 2. Recorded operations are not valid, but just the position needed to be updated.
+        // #needsRecord() returns false, but needsToBeShifted is true.
+        // 3. Recorded operations are not valid. Need to record operations. #needsRecord() returns
+        // true.
         RenderNode renderNode;
         boolean isDirty;
+        // Becomes true when recorded operations can be reused, but the position has to be updated.
+        boolean needsToBeShifted;
         public TextRenderNode(String name) {
-            isDirty = true;
             renderNode = RenderNode.create(name, null);
+            isDirty = true;
+            needsToBeShifted = true;
         }
         boolean needsRecord() {
             return isDirty || !renderNode.isValid();
@@ -1686,85 +1697,138 @@
             final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();
             final int indexFirstChangedBlock = dynamicLayout.getIndexFirstChangedBlock();
 
-            int endOfPreviousBlock = -1;
-            int searchStartIndex = 0;
-            for (int i = 0; i < numberOfBlocks; i++) {
-                int blockEndLine = blockEndLines[i];
-                int blockIndex = blockIndices[i];
-
-                final boolean blockIsInvalid = blockIndex == DynamicLayout.INVALID_BLOCK_INDEX;
-                if (blockIsInvalid) {
-                    blockIndex = getAvailableDisplayListIndex(blockIndices, numberOfBlocks,
-                            searchStartIndex);
-                    // Note how dynamic layout's internal block indices get updated from Editor
-                    blockIndices[i] = blockIndex;
-                    if (mTextRenderNodes[blockIndex] != null) {
-                        mTextRenderNodes[blockIndex].isDirty = true;
+            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;
                     }
-                    searchStartIndex = blockIndex + 1;
                 }
-
-                if (mTextRenderNodes[blockIndex] == null) {
-                    mTextRenderNodes[blockIndex] =
-                            new TextRenderNode("Text " + blockIndex);
-                }
-
-                final boolean blockDisplayListIsInvalid =
-                        mTextRenderNodes[blockIndex].needsRecord();
-                RenderNode blockDisplayList = mTextRenderNodes[blockIndex].renderNode;
-                if (i >= indexFirstChangedBlock || blockDisplayListIsInvalid) {
-                    final int blockBeginLine = endOfPreviousBlock + 1;
-                    final int top = layout.getLineTop(blockBeginLine);
-                    final int bottom = layout.getLineBottom(blockEndLine);
-                    int left = 0;
-                    int right = mTextView.getWidth();
-                    if (mTextView.getHorizontallyScrolling()) {
-                        float min = Float.MAX_VALUE;
-                        float max = Float.MIN_VALUE;
-                        for (int line = blockBeginLine; line <= blockEndLine; line++) {
-                            min = Math.min(min, layout.getLineLeft(line));
-                            max = Math.max(max, layout.getLineRight(line));
-                        }
-                        left = (int) min;
-                        right = (int) (max + 0.5f);
-                    }
-
-                    // Rebuild display list if it is invalid
-                    if (blockDisplayListIsInvalid) {
-                        final DisplayListCanvas displayListCanvas = blockDisplayList.start(
-                                right - left, bottom - top);
-                        try {
-                            // drawText is always relative to TextView's origin, this translation
-                            // brings this range of text back to the top left corner of the viewport
-                            displayListCanvas.translate(-left, -top);
-                            layout.drawText(displayListCanvas, blockBeginLine, blockEndLine);
-                            mTextRenderNodes[blockIndex].isDirty = false;
-                            // No need to untranslate, previous context is popped after
-                            // drawDisplayList
-                        } finally {
-                            blockDisplayList.end(displayListCanvas);
-                            // Same as drawDisplayList below, handled by our TextView's parent
-                            blockDisplayList.setClipToBounds(false);
-                        }
-                    }
-
-                    // Valid disply list whose index is >= indexFirstChangedBlock
-                    // only needs to update its drawing location.
-                    blockDisplayList.setLeftTopRightBottom(left, top, right, bottom);
-                }
-
-                ((DisplayListCanvas) canvas).drawRenderNode(blockDisplayList);
-
-                endOfPreviousBlock = blockEndLine;
             }
 
-            dynamicLayout.setIndexFirstChangedBlock(numberOfBlocks);
+            int startBlock = Arrays.binarySearch(blockEndLines, 0, numberOfBlocks, firstLine);
+            if (startBlock < 0) {
+                startBlock = -(startBlock + 1);
+            }
+            startBlock = Math.min(indexFirstChangedBlock, startBlock);
+
+            int startIndexToFindAvailableRenderNode = 0;
+            int lastIndex = numberOfBlocks;
+
+            for (int i = startBlock; i < numberOfBlocks; i++) {
+                final int blockIndex = blockIndices[i];
+                if (i >= indexFirstChangedBlock
+                        && blockIndex != DynamicLayout.INVALID_BLOCK_INDEX
+                        && mTextRenderNodes[blockIndex] != null) {
+                    mTextRenderNodes[blockIndex].needsToBeShifted = true;
+                }
+                if (blockEndLines[i] < firstLine) {
+                    // Blocks in [indexFirstChangedBlock, firstLine) are not redrawn here. They will
+                    // be redrawn after they get scrolled into drawing range.
+                    continue;
+                }
+                startIndexToFindAvailableRenderNode = drawHardwareAcceleratedInner(canvas, layout,
+                        highlight, highlightPaint, cursorOffsetVertical, blockEndLines,
+                        blockIndices, i, numberOfBlocks, startIndexToFindAvailableRenderNode);
+                if (blockEndLines[i] >= lastLine) {
+                    lastIndex = Math.max(indexFirstChangedBlock, i + 1);
+                    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 {
             // Boring layout is used for empty and hint text
             layout.drawText(canvas, firstLine, lastLine);
         }
     }
 
+    private int drawHardwareAcceleratedInner(Canvas canvas, Layout layout, Path highlight,
+            Paint highlightPaint, int cursorOffsetVertical, int[] blockEndLines,
+            int[] blockIndices, int blockInfoIndex, int numberOfBlocks,
+            int startIndexToFindAvailableRenderNode) {
+        final int blockEndLine = blockEndLines[blockInfoIndex];
+        int blockIndex = blockIndices[blockInfoIndex];
+
+        final boolean blockIsInvalid = blockIndex == DynamicLayout.INVALID_BLOCK_INDEX;
+        if (blockIsInvalid) {
+            blockIndex = getAvailableDisplayListIndex(blockIndices, numberOfBlocks,
+                    startIndexToFindAvailableRenderNode);
+            // Note how dynamic layout's internal block indices get updated from Editor
+            blockIndices[blockInfoIndex] = blockIndex;
+            if (mTextRenderNodes[blockIndex] != null) {
+                mTextRenderNodes[blockIndex].isDirty = true;
+            }
+            startIndexToFindAvailableRenderNode = blockIndex + 1;
+        }
+
+        if (mTextRenderNodes[blockIndex] == null) {
+            mTextRenderNodes[blockIndex] = new TextRenderNode("Text " + blockIndex);
+        }
+
+        final boolean blockDisplayListIsInvalid = mTextRenderNodes[blockIndex].needsRecord();
+        RenderNode blockDisplayList = mTextRenderNodes[blockIndex].renderNode;
+        if (mTextRenderNodes[blockIndex].needsToBeShifted || blockDisplayListIsInvalid) {
+            final int blockBeginLine = blockInfoIndex == 0 ?
+                    0 : blockEndLines[blockInfoIndex - 1] + 1;
+            final int top = layout.getLineTop(blockBeginLine);
+            final int bottom = layout.getLineBottom(blockEndLine);
+            int left = 0;
+            int right = mTextView.getWidth();
+            if (mTextView.getHorizontallyScrolling()) {
+                float min = Float.MAX_VALUE;
+                float max = Float.MIN_VALUE;
+                for (int line = blockBeginLine; line <= blockEndLine; line++) {
+                    min = Math.min(min, layout.getLineLeft(line));
+                    max = Math.max(max, layout.getLineRight(line));
+                }
+                left = (int) min;
+                right = (int) (max + 0.5f);
+            }
+
+            // Rebuild display list if it is invalid
+            if (blockDisplayListIsInvalid) {
+                final DisplayListCanvas displayListCanvas = blockDisplayList.start(
+                        right - left, bottom - top);
+                try {
+                    // drawText is always relative to TextView's origin, this translation
+                    // brings this range of text back to the top left corner of the viewport
+                    displayListCanvas.translate(-left, -top);
+                    layout.drawText(displayListCanvas, blockBeginLine, blockEndLine);
+                    mTextRenderNodes[blockIndex].isDirty = false;
+                    // No need to untranslate, previous context is popped after
+                    // drawDisplayList
+                } finally {
+                    blockDisplayList.end(displayListCanvas);
+                    // Same as drawDisplayList below, handled by our TextView's parent
+                    blockDisplayList.setClipToBounds(false);
+                }
+            }
+
+            // Valid display list only needs to update its drawing location.
+            blockDisplayList.setLeftTopRightBottom(left, top, right, bottom);
+            mTextRenderNodes[blockIndex].needsToBeShifted = false;
+        }
+        ((DisplayListCanvas) canvas).drawRenderNode(blockDisplayList);
+        return startIndexToFindAvailableRenderNode;
+    }
+
     private int getAvailableDisplayListIndex(int[] blockIndices, int numberOfBlocks,
             int searchStartIndex) {
         int length = mTextRenderNodes.length;
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) {