Merge "Bug 5250788: LatinIME slows down as amount of Text increases"
diff --git a/core/java/android/text/CharSequenceIterator.java b/core/java/android/text/CharSequenceIterator.java
deleted file mode 100644
index 4b8ac10..0000000
--- a/core/java/android/text/CharSequenceIterator.java
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Copyright (C) 2011 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 java.text.CharacterIterator;
-
-/** {@hide} */
-public class CharSequenceIterator implements CharacterIterator {
-    private final CharSequence mValue;
-
-    private final int mLength;
-    private int mIndex;
-
-    public CharSequenceIterator(CharSequence value) {
-        mValue = value;
-        mLength = value.length();
-        mIndex = 0;
-    }
-
-    @Override
-    public Object clone() {
-        try {
-            return super.clone();
-        } catch (CloneNotSupportedException e) {
-            throw new AssertionError(e);
-        }
-    }
-
-    /** {@inheritDoc} */
-    public char current() {
-        if (mIndex == mLength) {
-            return DONE;
-        }
-        return mValue.charAt(mIndex);
-    }
-
-    /** {@inheritDoc} */
-    public int getBeginIndex() {
-        return 0;
-    }
-
-    /** {@inheritDoc} */
-    public int getEndIndex() {
-        return mLength;
-    }
-
-    /** {@inheritDoc} */
-    public int getIndex() {
-        return mIndex;
-    }
-
-    /** {@inheritDoc} */
-    public char first() {
-        return setIndex(0);
-    }
-
-    /** {@inheritDoc} */
-    public char last() {
-        return setIndex(mLength - 1);
-    }
-
-    /** {@inheritDoc} */
-    public char next() {
-        if (mIndex == mLength) {
-            return DONE;
-        }
-        return setIndex(mIndex + 1);
-    }
-
-    /** {@inheritDoc} */
-    public char previous() {
-        if (mIndex == 0) {
-            return DONE;
-        }
-        return setIndex(mIndex - 1);
-    }
-
-    /** {@inheritDoc} */
-    public char setIndex(int index) {
-        if ((index < 0) || (index > mLength)) {
-            throw new IllegalArgumentException("Valid range is [" + 0 + "..." + mLength + "]");
-        }
-        mIndex = index;
-        return current();
-    }
-}
diff --git a/core/java/android/text/method/ArrowKeyMovementMethod.java b/core/java/android/text/method/ArrowKeyMovementMethod.java
index b8728ee..e93039b 100644
--- a/core/java/android/text/method/ArrowKeyMovementMethod.java
+++ b/core/java/android/text/method/ArrowKeyMovementMethod.java
@@ -35,11 +35,11 @@
                 (MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SELECTING) != 0));
     }
 
-    private int getCurrentLineTop(Spannable buffer, Layout layout) {
+    private static int getCurrentLineTop(Spannable buffer, Layout layout) {
         return layout.getLineTop(layout.getLineForOffset(Selection.getSelectionEnd(buffer)));
     }
 
-    private int getPageHeight(TextView widget) {
+    private static int getPageHeight(TextView widget) {
         // This calculation does not take into account the view transformations that
         // may have been applied to the child or its containers.  In case of scaling or
         // rotation, the calculated page height may be incorrect.
@@ -196,14 +196,16 @@
     /** {@hide} */
     @Override
     protected boolean leftWord(TextView widget, Spannable buffer) {
-        mWordIterator.setCharSequence(buffer);
+        final int selectionEnd = widget.getSelectionEnd();
+        mWordIterator.setCharSequence(buffer, selectionEnd, selectionEnd);
         return Selection.moveToPreceding(buffer, mWordIterator, isSelecting(buffer));
     }
 
     /** {@hide} */
     @Override
     protected boolean rightWord(TextView widget, Spannable buffer) {
-        mWordIterator.setCharSequence(buffer);
+        final int selectionEnd = widget.getSelectionEnd();
+        mWordIterator.setCharSequence(buffer, selectionEnd, selectionEnd);
         return Selection.moveToFollowing(buffer, mWordIterator, isSelecting(buffer));
     }
 
diff --git a/core/java/android/text/method/WordIterator.java b/core/java/android/text/method/WordIterator.java
index af524ee..239d9e8 100644
--- a/core/java/android/text/method/WordIterator.java
+++ b/core/java/android/text/method/WordIterator.java
@@ -17,14 +17,9 @@
 
 package android.text.method;
 
-import android.text.CharSequenceIterator;
-import android.text.Editable;
 import android.text.Selection;
-import android.text.Spanned;
-import android.text.TextWatcher;
 
 import java.text.BreakIterator;
-import java.text.CharacterIterator;
 import java.util.Locale;
 
 /**
@@ -36,8 +31,11 @@
  * {@hide}
  */
 public class WordIterator implements Selection.PositionIterator {
-    private CharSequence mCurrent;
-    private boolean mCurrentDirty = false;
+    // Size of the window for the word iterator, should be greater than the longest word's length
+    private static final int WINDOW_WIDTH = 50;
+
+    private String mString;
+    private int mOffsetShift;
 
     private BreakIterator mIterator;
 
@@ -56,70 +54,40 @@
         mIterator = BreakIterator.getWordInstance(locale);
     }
 
-    private final TextWatcher mWatcher = new TextWatcher() {
-        /** {@inheritDoc} */
-        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
-            // ignored
-        }
+    public void setCharSequence(CharSequence charSequence, int start, int end) {
+        mOffsetShift = Math.max(0, start - WINDOW_WIDTH);
+        final int windowEnd = Math.min(charSequence.length(), end + WINDOW_WIDTH);
 
-        /** {@inheritDoc} */
-        public void onTextChanged(CharSequence s, int start, int before, int count) {
-            mCurrentDirty = true;
-        }
-
-        /** {@inheritDoc} */
-        public void afterTextChanged(Editable s) {
-            // ignored
-        }
-    };
-
-    public void setCharSequence(CharSequence incoming) {
-        // When incoming is different object, move listeners to new sequence
-        // and mark as dirty so we reload contents.
-        if (mCurrent != incoming) {
-            if (mCurrent instanceof Editable) {
-                ((Editable) mCurrent).removeSpan(mWatcher);
-            }
-
-            if (incoming instanceof Editable) {
-                ((Editable) incoming).setSpan(
-                        mWatcher, 0, incoming.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-            }
-
-            mCurrent = incoming;
-            mCurrentDirty = true;
-        }
-
-        if (mCurrentDirty) {
-            final CharacterIterator charIterator = new CharSequenceIterator(mCurrent);
-            mIterator.setText(charIterator);
-
-            mCurrentDirty = false;
-        }
+        mString = charSequence.toString().substring(mOffsetShift, windowEnd);
+        mIterator.setText(mString);
     }
 
     /** {@inheritDoc} */
     public int preceding(int offset) {
+        int shiftedOffset = offset - mOffsetShift;
         do {
-            offset = mIterator.preceding(offset);
-            if (offset == BreakIterator.DONE || isOnLetterOrDigit(offset)) {
-                break;
+            shiftedOffset = mIterator.preceding(shiftedOffset);
+            if (shiftedOffset == BreakIterator.DONE) {
+                return BreakIterator.DONE;
+            }
+            if (isOnLetterOrDigit(shiftedOffset)) {
+                return shiftedOffset + mOffsetShift;
             }
         } while (true);
-
-        return offset;
     }
 
     /** {@inheritDoc} */
     public int following(int offset) {
+        int shiftedOffset = offset - mOffsetShift;
         do {
-            offset = mIterator.following(offset);
-            if (offset == BreakIterator.DONE || isAfterLetterOrDigit(offset)) {
-                break;
+            shiftedOffset = mIterator.following(shiftedOffset);
+            if (shiftedOffset == BreakIterator.DONE) {
+                return BreakIterator.DONE;
+            }
+            if (isAfterLetterOrDigit(shiftedOffset)) {
+                return shiftedOffset + mOffsetShift;
             }
         } while (true);
-
-        return offset;
     }
 
     /** If <code>offset</code> is within a word, returns the index of the first character of that
@@ -135,17 +103,18 @@
      * @throws IllegalArgumentException is offset is not valid.
      */
     public int getBeginning(int offset) {
-        checkOffsetIsValid(offset);
+        final int shiftedOffset = offset - mOffsetShift;
+        checkOffsetIsValid(shiftedOffset);
 
-        if (isOnLetterOrDigit(offset)) {
-            if (mIterator.isBoundary(offset)) {
-                return offset;
+        if (isOnLetterOrDigit(shiftedOffset)) {
+            if (mIterator.isBoundary(shiftedOffset)) {
+                return shiftedOffset + mOffsetShift;
             } else {
-                return mIterator.preceding(offset);
+                return mIterator.preceding(shiftedOffset) + mOffsetShift;
             }
         } else {
-            if (isAfterLetterOrDigit(offset)) {
-                return mIterator.preceding(offset);
+            if (isAfterLetterOrDigit(shiftedOffset)) {
+                return mIterator.preceding(shiftedOffset) + mOffsetShift;
             }
         }
         return BreakIterator.DONE;
@@ -164,58 +133,44 @@
      * @throws IllegalArgumentException is offset is not valid.
      */
     public int getEnd(int offset) {
-        checkOffsetIsValid(offset);
+        final int shiftedOffset = offset - mOffsetShift;
+        checkOffsetIsValid(shiftedOffset);
 
-        if (isAfterLetterOrDigit(offset)) {
-            if (mIterator.isBoundary(offset)) {
-                return offset;
+        if (isAfterLetterOrDigit(shiftedOffset)) {
+            if (mIterator.isBoundary(shiftedOffset)) {
+                return shiftedOffset + mOffsetShift;
             } else {
-                return mIterator.following(offset);
+                return mIterator.following(shiftedOffset) + mOffsetShift;
             }
         } else {
-            if (isOnLetterOrDigit(offset)) {
-                return mIterator.following(offset);
+            if (isOnLetterOrDigit(shiftedOffset)) {
+                return mIterator.following(shiftedOffset) + mOffsetShift;
             }
         }
         return BreakIterator.DONE;
     }
 
-    private boolean isAfterLetterOrDigit(int offset) {
-        if (offset - 1 >= 0) {
-            final char previousChar = mCurrent.charAt(offset - 1);
-            if (Character.isLetterOrDigit(previousChar)) return true;
-            if (offset - 2 >= 0) {
-                final char previousPreviousChar = mCurrent.charAt(offset - 2);
-                if (Character.isSurrogatePair(previousPreviousChar, previousChar)) {
-                    final int codePoint = Character.toCodePoint(previousPreviousChar, previousChar);
-                    return Character.isLetterOrDigit(codePoint);
-                }
-            }
+    private boolean isAfterLetterOrDigit(int shiftedOffset) {
+        if (shiftedOffset >= 1 && shiftedOffset <= mString.length()) {
+            final int codePoint = mString.codePointBefore(shiftedOffset);
+            if (Character.isLetterOrDigit(codePoint)) return true;
         }
         return false;
     }
 
-    private boolean isOnLetterOrDigit(int offset) {
-        final int length = mCurrent.length();
-        if (offset < length) {
-            final char currentChar = mCurrent.charAt(offset);
-            if (Character.isLetterOrDigit(currentChar)) return true;
-            if (offset + 1 < length) {
-                final char nextChar = mCurrent.charAt(offset + 1);
-                if (Character.isSurrogatePair(currentChar, nextChar)) {
-                    final int codePoint = Character.toCodePoint(currentChar, nextChar);
-                    return Character.isLetterOrDigit(codePoint);
-                }
-            }
+    private boolean isOnLetterOrDigit(int shiftedOffset) {
+        if (shiftedOffset >= 0 && shiftedOffset < mString.length()) {
+            final int codePoint = mString.codePointAt(shiftedOffset);
+            if (Character.isLetterOrDigit(codePoint)) return true;
         }
         return false;
     }
 
-    private void checkOffsetIsValid(int offset) {
-        if (offset < 0 || offset > mCurrent.length()) {
-            final String message = "Invalid offset: " + offset +
-                    ". Valid range is [0, " + mCurrent.length() + "]";
-            throw new IllegalArgumentException(message);
+    private void checkOffsetIsValid(int shiftedOffset) {
+        if (shiftedOffset < 0 || shiftedOffset > mString.length()) {
+            throw new IllegalArgumentException("Invalid offset: " + (shiftedOffset + mOffsetShift) +
+                    ". Valid range is [" + mOffsetShift + ", " + (mString.length() + mOffsetShift) +
+                    "]");
         }
     }
 }
diff --git a/core/java/android/widget/SpellChecker.java b/core/java/android/widget/SpellChecker.java
index ce17184..62b078f 100644
--- a/core/java/android/widget/SpellChecker.java
+++ b/core/java/android/widget/SpellChecker.java
@@ -20,6 +20,7 @@
 import android.text.Editable;
 import android.text.Selection;
 import android.text.Spanned;
+import android.text.method.WordIterator;
 import android.text.style.SpellCheckSpan;
 import android.text.style.SuggestionSpan;
 import android.view.textservice.SpellCheckerSession;
@@ -30,6 +31,8 @@
 
 import com.android.internal.util.ArrayUtils;
 
+import java.text.BreakIterator;
+
 
 /**
  * Helper class for TextView. Bridge between the TextView and the Dictionnary service.
@@ -38,23 +41,30 @@
  */
 public class SpellChecker implements SpellCheckerSessionListener {
 
+    private final static int MAX_SPELL_BATCH_SIZE = 50;
+
     private final TextView mTextView;
+    private final Editable mText;
 
     final SpellCheckerSession mSpellCheckerSession;
     final int mCookie;
 
     // Paired arrays for the (id, spellCheckSpan) pair. A negative id means the associated
     // SpellCheckSpan has been recycled and can be-reused.
-    // May contain null SpellCheckSpans after a given index.
+    // Contains null SpellCheckSpans after index mLength.
     private int[] mIds;
     private SpellCheckSpan[] mSpellCheckSpans;
     // The mLength first elements of the above arrays have been initialized
     private int mLength;
 
+    // Parsers on chunck of text, cutting text into words that will be checked
+    private SpellParser[] mSpellParsers = new SpellParser[0];
+
     private int mSpanSequenceCounter = 0;
 
     public SpellChecker(TextView textView) {
         mTextView = textView;
+        mText = (Editable) textView.getText();
 
         final TextServicesManager textServicesManager = (TextServicesManager) textView.getContext().
                 getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE);
@@ -62,7 +72,7 @@
                 null /* not currently used by the textServicesManager */,
                 null /* null locale means use the languages defined in Settings
                         if referToSpellCheckerLanguageSettings is true */,
-                this, true /* means use the languages defined in Settings */);
+                        this, true /* means use the languages defined in Settings */);
         mCookie = hashCode();
 
         // Arbitrary: 4 simultaneous spell check spans. Will automatically double size on demand
@@ -76,7 +86,7 @@
      * @return true if a spell checker session has successfully been created. Returns false if not,
      * for instance when spell checking has been disabled in settings.
      */
-    public boolean isSessionActive() {
+    private boolean isSessionActive() {
         return mSpellCheckerSession != null;
     }
 
@@ -84,6 +94,11 @@
         if (mSpellCheckerSession != null) {
             mSpellCheckerSession.close();
         }
+
+        final int length = mSpellParsers.length;
+        for (int i = 0; i < length; i++) {
+            mSpellParsers[i].close();
+        }
     }
 
     private int nextSpellCheckSpanIndex() {
@@ -106,10 +121,9 @@
         return mLength - 1;
     }
 
-    public void addSpellCheckSpan(int wordStart, int wordEnd) {
+    private void addSpellCheckSpan(int start, int end) {
         final int index = nextSpellCheckSpanIndex();
-        ((Editable) mTextView.getText()).setSpan(mSpellCheckSpans[index], wordStart, wordEnd,
-                Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+        mText.setSpan(mSpellCheckSpans[index], start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
         mIds[index] = mSpanSequenceCounter++;
     }
 
@@ -127,12 +141,35 @@
         spellCheck();
     }
 
-    public void spellCheck() {
+    public void spellCheck(int start, int end) {
+        if (!isSessionActive()) return;
+
+        final int length = mSpellParsers.length;
+        for (int i = 0; i < length; i++) {
+            final SpellParser spellParser = mSpellParsers[i];
+            if (spellParser.isDone()) {
+                spellParser.init(start, end);
+                spellParser.parse();
+                return;
+            }
+        }
+
+        // No available parser found in pool, create a new one
+        SpellParser[] newSpellParsers = new SpellParser[length + 1];
+        System.arraycopy(mSpellParsers, 0, newSpellParsers, 0, length);
+        mSpellParsers = newSpellParsers;
+
+        SpellParser spellParser = new SpellParser();
+        mSpellParsers[length] = spellParser;
+        spellParser.init(start, end);
+        spellParser.parse();
+    }
+
+    private void spellCheck() {
         if (mSpellCheckerSession == null) return;
 
-        final Editable editable = (Editable) mTextView.getText();
-        final int selectionStart = Selection.getSelectionStart(editable);
-        final int selectionEnd = Selection.getSelectionEnd(editable);
+        final int selectionStart = Selection.getSelectionStart(mText);
+        final int selectionEnd = Selection.getSelectionEnd(mText);
 
         TextInfo[] textInfos = new TextInfo[mLength];
         int textInfosCount = 0;
@@ -141,19 +178,19 @@
             final SpellCheckSpan spellCheckSpan = mSpellCheckSpans[i];
             if (spellCheckSpan.isSpellCheckInProgress()) continue;
 
-            final int start = editable.getSpanStart(spellCheckSpan);
-            final int end = editable.getSpanEnd(spellCheckSpan);
+            final int start = mText.getSpanStart(spellCheckSpan);
+            final int end = mText.getSpanEnd(spellCheckSpan);
 
             // Do not check this word if the user is currently editing it
             if (start >= 0 && end > start && (selectionEnd < start || selectionStart > end)) {
-                final String word = editable.subSequence(start, end).toString();
+                final String word = mText.subSequence(start, end).toString();
                 spellCheckSpan.setSpellCheckInProgress(true);
                 textInfos[textInfosCount++] = new TextInfo(word, mCookie, mIds[i]);
             }
         }
 
         if (textInfosCount > 0) {
-            if (textInfosCount < mLength) {
+            if (textInfosCount < textInfos.length) {
                 TextInfo[] textInfosCopy = new TextInfo[textInfosCount];
                 System.arraycopy(textInfos, 0, textInfosCopy, 0, textInfosCount);
                 textInfos = textInfosCopy;
@@ -165,7 +202,6 @@
 
     @Override
     public void onGetSuggestions(SuggestionsInfo[] results) {
-        final Editable editable = (Editable) mTextView.getText();
         for (int i = 0; i < results.length; i++) {
             SuggestionsInfo suggestionsInfo = results[i];
             if (suggestionsInfo.getCookie() != mCookie) continue;
@@ -181,27 +217,35 @@
 
                     SpellCheckSpan spellCheckSpan = mSpellCheckSpans[j];
                     if (!isInDictionary && looksLikeTypo) {
-                        createMisspelledSuggestionSpan(editable, suggestionsInfo, spellCheckSpan);
+                        createMisspelledSuggestionSpan(suggestionsInfo, spellCheckSpan);
                     }
-                    editable.removeSpan(spellCheckSpan);
+                    mText.removeSpan(spellCheckSpan);
                     break;
                 }
             }
         }
+
+        final int length = mSpellParsers.length;
+        for (int i = 0; i < length; i++) {
+            final SpellParser spellParser = mSpellParsers[i];
+            if (!spellParser.isDone()) {
+                spellParser.parse();
+            }
+        }
     }
 
-    private void createMisspelledSuggestionSpan(Editable editable, SuggestionsInfo suggestionsInfo,
+    private void createMisspelledSuggestionSpan(SuggestionsInfo suggestionsInfo,
             SpellCheckSpan spellCheckSpan) {
-        final int start = editable.getSpanStart(spellCheckSpan);
-        final int end = editable.getSpanEnd(spellCheckSpan);
+        final int start = mText.getSpanStart(spellCheckSpan);
+        final int end = mText.getSpanEnd(spellCheckSpan);
 
         // Other suggestion spans may exist on that region, with identical suggestions, filter
         // them out to avoid duplicates. First, filter suggestion spans on that exact region.
-        SuggestionSpan[] suggestionSpans = editable.getSpans(start, end, SuggestionSpan.class);
+        SuggestionSpan[] suggestionSpans = mText.getSpans(start, end, SuggestionSpan.class);
         final int length = suggestionSpans.length;
         for (int i = 0; i < length; i++) {
-            final int spanStart = editable.getSpanStart(suggestionSpans[i]);
-            final int spanEnd = editable.getSpanEnd(suggestionSpans[i]);
+            final int spanStart = mText.getSpanStart(suggestionSpans[i]);
+            final int spanEnd = mText.getSpanEnd(suggestionSpans[i]);
             if (spanStart != start || spanEnd != end) {
                 suggestionSpans[i] = null;
                 break;
@@ -249,9 +293,132 @@
 
         SuggestionSpan suggestionSpan = new SuggestionSpan(mTextView.getContext(), suggestions,
                 SuggestionSpan.FLAG_EASY_CORRECT | SuggestionSpan.FLAG_MISSPELLED);
-        editable.setSpan(suggestionSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+        mText.setSpan(suggestionSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
 
         // TODO limit to the word rectangle region
         mTextView.invalidate();
     }
+
+    private class SpellParser {
+        private WordIterator mWordIterator = new WordIterator(/*TODO Locale*/);
+        private Object mRange = new Object();
+
+        public void init(int start, int end) {
+            mText.setSpan(mRange, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+        }
+
+        public void close() {
+            mText.removeSpan(mRange);
+        }
+
+        public boolean isDone() {
+            return mText.getSpanStart(mRange) < 0;
+        }
+
+        public void parse() {
+            // Iterate over the newly added text and schedule new SpellCheckSpans
+            final int start = mText.getSpanStart(mRange);
+            final int end = mText.getSpanEnd(mRange);
+            mWordIterator.setCharSequence(mText, start, end);
+
+            // Move back to the beginning of the current word, if any
+            int wordStart = mWordIterator.preceding(start);
+            int wordEnd;
+            if (wordStart == BreakIterator.DONE) {
+                wordEnd = mWordIterator.following(start);
+                if (wordEnd != BreakIterator.DONE) {
+                    wordStart = mWordIterator.getBeginning(wordEnd);
+                }
+            } else {
+                wordEnd = mWordIterator.getEnd(wordStart);
+            }
+            if (wordEnd == BreakIterator.DONE) {
+                mText.removeSpan(mRange);
+                return;
+            }
+
+            // We need to expand by one character because we want to include the spans that
+            // end/start at position start/end respectively.
+            SpellCheckSpan[] spellCheckSpans = mText.getSpans(start-1, end+1, SpellCheckSpan.class);
+            SuggestionSpan[] suggestionSpans = mText.getSpans(start-1, end+1, SuggestionSpan.class);
+
+            int nbWordsChecked = 0;
+            boolean scheduleOtherSpellCheck = false;
+
+            while (wordStart <= end) {
+                if (wordEnd >= start && wordEnd > wordStart) {
+                    // A new word has been created across the interval boundaries with this edit.
+                    // Previous spans (ended on start / started on end) removed, not valid anymore
+                    if (wordStart < start && wordEnd > start) {
+                        removeSpansAt(start, spellCheckSpans);
+                        removeSpansAt(start, suggestionSpans);
+                    }
+
+                    if (wordStart < end && wordEnd > end) {
+                        removeSpansAt(end, spellCheckSpans);
+                        removeSpansAt(end, suggestionSpans);
+                    }
+
+                    // Do not create new boundary spans if they already exist
+                    boolean createSpellCheckSpan = true;
+                    if (wordEnd == start) {
+                        for (int i = 0; i < spellCheckSpans.length; i++) {
+                            final int spanEnd = mText.getSpanEnd(spellCheckSpans[i]);
+                            if (spanEnd == start) {
+                                createSpellCheckSpan = false;
+                                break;
+                            }
+                        }
+                    }
+
+                    if (wordStart == end) {
+                        for (int i = 0; i < spellCheckSpans.length; i++) {
+                            final int spanStart = mText.getSpanStart(spellCheckSpans[i]);
+                            if (spanStart == end) {
+                                createSpellCheckSpan = false;
+                                break;
+                            }
+                        }
+                    }
+
+                    if (createSpellCheckSpan) {
+                        if (nbWordsChecked == MAX_SPELL_BATCH_SIZE) {
+                            scheduleOtherSpellCheck = true;
+                            break;
+                        }
+                        addSpellCheckSpan(wordStart, wordEnd);
+                        nbWordsChecked++;
+                    }
+                }
+
+                // iterate word by word
+                wordEnd = mWordIterator.following(wordEnd);
+                if (wordEnd == BreakIterator.DONE) break;
+                wordStart = mWordIterator.getBeginning(wordEnd);
+                if (wordStart == BreakIterator.DONE) {
+                    break;
+                }
+            }
+
+            if (scheduleOtherSpellCheck) {
+                mText.setSpan(mRange, wordStart, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+            } else {
+                mText.removeSpan(mRange);
+            }
+
+            spellCheck();
+        }
+
+        private <T> void removeSpansAt(int offset, T[] spans) {
+            final int length = spans.length;
+            for (int i = 0; i < length; i++) {
+                final T span = spans[i];
+                final int start = mText.getSpanStart(span);
+                if (start > offset) continue;
+                final int end = mText.getSpanEnd(span);
+                if (end < offset) continue;
+                mText.removeSpan(span);
+            }
+        }
+    }
 }
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 68de2e9..41daf70 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -353,8 +353,6 @@
     // Set when this TextView gained focus with some text selected. Will start selection mode.
     private boolean mCreatedWithASelection = false;
 
-    // Size of the window for the word iterator, should be greater than the longest word's length
-    private static final int WORD_ITERATOR_WINDOW_WIDTH = 50;
     private WordIterator mWordIterator;
 
     private SpellChecker mSpellChecker;
@@ -6124,7 +6122,7 @@
      * not the full view width with padding.
      * {@hide}
      */
-    protected void makeNewLayout(int w, int hintWidth,
+    protected void makeNewLayout(int wantWidth, int hintWidth,
                                  BoringLayout.Metrics boring,
                                  BoringLayout.Metrics hintBoring,
                                  int ellipsisWidth, boolean bringIntoView) {
@@ -6136,8 +6134,8 @@
 
         mHighlightPathBogus = true;
 
-        if (w < 0) {
-            w = 0;
+        if (wantWidth < 0) {
+            wantWidth = 0;
         }
         if (hintWidth < 0) {
             hintWidth = 0;
@@ -6157,12 +6155,12 @@
             resolveTextDirection();
         }
 
-        mLayout = makeSingleLayout(w, boring, ellipsisWidth, alignment, shouldEllipsize,
+        mLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment, shouldEllipsize,
                 effectiveEllipsize, effectiveEllipsize == mEllipsize);
         if (switchEllipsize) {
             TruncateAt oppositeEllipsize = effectiveEllipsize == TruncateAt.MARQUEE ?
                     TruncateAt.END : TruncateAt.MARQUEE;
-            mSavedMarqueeModeLayout = makeSingleLayout(w, boring, ellipsisWidth, alignment,
+            mSavedMarqueeModeLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment,
                     shouldEllipsize, oppositeEllipsize, effectiveEllipsize != mEllipsize);
         }
 
@@ -6170,7 +6168,7 @@
         mHintLayout = null;
 
         if (mHint != null) {
-            if (shouldEllipsize) hintWidth = w;
+            if (shouldEllipsize) hintWidth = wantWidth;
 
             if (hintBoring == UNKNOWN_BORING) {
                 hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir,
@@ -6254,12 +6252,12 @@
         prepareCursorControllers();
     }
 
-    private Layout makeSingleLayout(int w, BoringLayout.Metrics boring, int ellipsisWidth,
+    private Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth,
             Layout.Alignment alignment, boolean shouldEllipsize, TruncateAt effectiveEllipsize,
             boolean useSaved) {
         Layout result = null;
         if (mText instanceof Spannable) {
-            result = new DynamicLayout(mText, mTransformed, mTextPaint, w,
+            result = new DynamicLayout(mText, mTransformed, mTextPaint, wantWidth,
                     alignment, mTextDir, mSpacingMult,
                     mSpacingAdd, mIncludePad, mInput == null ? effectiveEllipsize : null,
                             ellipsisWidth);
@@ -6272,53 +6270,53 @@
             }
 
             if (boring != null) {
-                if (boring.width <= w &&
+                if (boring.width <= wantWidth &&
                         (effectiveEllipsize == null || boring.width <= ellipsisWidth)) {
                     if (useSaved && mSavedLayout != null) {
                         result = mSavedLayout.replaceOrMake(mTransformed, mTextPaint,
-                                w, alignment, mSpacingMult, mSpacingAdd,
+                                wantWidth, alignment, mSpacingMult, mSpacingAdd,
                                 boring, mIncludePad);
                     } else {
                         result = BoringLayout.make(mTransformed, mTextPaint,
-                                w, alignment, mSpacingMult, mSpacingAdd,
+                                wantWidth, alignment, mSpacingMult, mSpacingAdd,
                                 boring, mIncludePad);
                     }
 
                     if (useSaved) {
                         mSavedLayout = (BoringLayout) result;
                     }
-                } else if (shouldEllipsize && boring.width <= w) {
+                } else if (shouldEllipsize && boring.width <= wantWidth) {
                     if (useSaved && mSavedLayout != null) {
                         result = mSavedLayout.replaceOrMake(mTransformed, mTextPaint,
-                                w, alignment, mSpacingMult, mSpacingAdd,
+                                wantWidth, alignment, mSpacingMult, mSpacingAdd,
                                 boring, mIncludePad, effectiveEllipsize,
                                 ellipsisWidth);
                     } else {
                         result = BoringLayout.make(mTransformed, mTextPaint,
-                                w, alignment, mSpacingMult, mSpacingAdd,
+                                wantWidth, alignment, mSpacingMult, mSpacingAdd,
                                 boring, mIncludePad, effectiveEllipsize,
                                 ellipsisWidth);
                     }
                 } else if (shouldEllipsize) {
                     result = new StaticLayout(mTransformed,
                             0, mTransformed.length(),
-                            mTextPaint, w, alignment, mTextDir, mSpacingMult,
+                            mTextPaint, wantWidth, alignment, mTextDir, mSpacingMult,
                             mSpacingAdd, mIncludePad, effectiveEllipsize,
                             ellipsisWidth, mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
                 } else {
                     result = new StaticLayout(mTransformed, mTextPaint,
-                            w, alignment, mTextDir, mSpacingMult, mSpacingAdd,
+                            wantWidth, alignment, mTextDir, mSpacingMult, mSpacingAdd,
                             mIncludePad);
                 }
             } else if (shouldEllipsize) {
                 result = new StaticLayout(mTransformed,
                         0, mTransformed.length(),
-                        mTextPaint, w, alignment, mTextDir, mSpacingMult,
+                        mTextPaint, wantWidth, alignment, mTextDir, mSpacingMult,
                         mSpacingAdd, mIncludePad, effectiveEllipsize,
                         ellipsisWidth, mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
             } else {
                 result = new StaticLayout(mTransformed, mTextPaint,
-                        w, alignment, mTextDir, mSpacingMult, mSpacingAdd,
+                        wantWidth, alignment, mTextDir, mSpacingMult, mSpacingAdd,
                         mIncludePad);
             }
         }
@@ -7749,98 +7747,8 @@
      * Create new SpellCheckSpans on the modified region.
      */
     private void updateSpellCheckSpans(int start, int end) {
-        if (!isTextEditable() || !isSuggestionsEnabled() || !getSpellChecker().isSessionActive())
-            return;
-        Editable text = (Editable) mText;
-
-        final int shift = prepareWordIterator(start, end);
-        final int shiftedStart = start - shift;
-        final int shiftedEnd = end - shift;
-
-        // Move back to the beginning of the current word, if any
-        int wordStart = mWordIterator.preceding(shiftedStart);
-        int wordEnd;
-        if (wordStart == BreakIterator.DONE) {
-            wordEnd = mWordIterator.following(shiftedStart);
-            if (wordEnd != BreakIterator.DONE) {
-                wordStart = mWordIterator.getBeginning(wordEnd);
-            }
-        } else {
-            wordEnd = mWordIterator.getEnd(wordStart);
-        }
-        if (wordEnd == BreakIterator.DONE) {
-            return;
-        }
-
-        // We need to expand by one character because we want to include the spans that end/start
-        // at position start/end respectively.
-        SpellCheckSpan[] spellCheckSpans = text.getSpans(start - 1, end + 1, SpellCheckSpan.class);
-        SuggestionSpan[] suggestionSpans = text.getSpans(start - 1, end + 1, SuggestionSpan.class);
-        final int numberOfSpellCheckSpans = spellCheckSpans.length;
-
-        // Iterate over the newly added text and schedule new SpellCheckSpans
-        while (wordStart <= shiftedEnd) {
-            if (wordEnd >= shiftedStart && wordEnd > wordStart) {
-                // A new word has been created across the interval boundaries. Remove previous spans
-                if (wordStart < shiftedStart && wordEnd > shiftedStart) {
-                    removeSpansAt(start, spellCheckSpans, text);
-                    removeSpansAt(start, suggestionSpans, text);
-                }
-
-                if (wordStart < shiftedEnd && wordEnd > shiftedEnd) {
-                    removeSpansAt(end, spellCheckSpans, text);
-                    removeSpansAt(end, suggestionSpans, text);
-                }
-
-                // Do not create new boundary spans if they already exist
-                boolean createSpellCheckSpan = true;
-                if (wordEnd == shiftedStart) {
-                    for (int i = 0; i < numberOfSpellCheckSpans; i++) {
-                        final int spanEnd = text.getSpanEnd(spellCheckSpans[i]);
-                        if (spanEnd == start) {
-                            createSpellCheckSpan = false;
-                            break;
-                        }
-                    }
-                }
-
-                if (wordStart == shiftedEnd) {
-                    for (int i = 0; i < numberOfSpellCheckSpans; i++) {
-                        final int spanStart = text.getSpanStart(spellCheckSpans[i]);
-                        if (spanStart == end) {
-                            createSpellCheckSpan = false;
-                            break;
-                        }
-                    }
-                }
-
-                if (createSpellCheckSpan) {
-                    mSpellChecker.addSpellCheckSpan(wordStart + shift, wordEnd + shift);
-                }
-            }
-
-            // iterate word by word
-            wordEnd = mWordIterator.following(wordEnd);
-            if (wordEnd == BreakIterator.DONE) break;
-            wordStart = mWordIterator.getBeginning(wordEnd);
-            if (wordStart == BreakIterator.DONE) {
-                Log.e(LOG_TAG, "No word beginning from " + (wordEnd + shift) + "in " + mText);
-                break;
-            }
-        }
-
-        mSpellChecker.spellCheck();
-    }
-
-    private static <T> void removeSpansAt(int offset, T[] spans, Editable text) {
-        final int length = spans.length;
-        for (int i = 0; i < length; i++) {
-            final T span = spans[i];
-            final int start = text.getSpanStart(span);
-            if (start > offset) continue;
-            final int end = text.getSpanEnd(span);
-            if (end < offset) continue;
-            text.removeSpan(span);
+        if (isTextEditable() && isSuggestionsEnabled()) {
+            getSpellChecker().spellCheck(start, end);
         }
     }
 
@@ -8930,15 +8838,16 @@
             selectionStart = ((Spanned) mText).getSpanStart(urlSpan);
             selectionEnd = ((Spanned) mText).getSpanEnd(urlSpan);
         } else {
-            final int shift = prepareWordIterator(minOffset, maxOffset);
+            if (mWordIterator == null) {
+                mWordIterator = new WordIterator();
+            }
+            mWordIterator.setCharSequence(mText, minOffset, maxOffset);
 
-            selectionStart = mWordIterator.getBeginning(minOffset - shift);
+            selectionStart = mWordIterator.getBeginning(minOffset);
             if (selectionStart == BreakIterator.DONE) return false;
-            selectionStart += shift;
 
-            selectionEnd = mWordIterator.getEnd(maxOffset - shift);
+            selectionEnd = mWordIterator.getEnd(maxOffset);
             if (selectionEnd == BreakIterator.DONE) return false;
-            selectionEnd += shift;
 
             if (selectionStart == selectionEnd) {
                 // Possible when the word iterator does not properly handle the text's language
@@ -8977,18 +8886,6 @@
         return packRangeInLong(offset,  offset);
     }
 
-    int prepareWordIterator(int start, int end) {
-        if (mWordIterator == null) {
-            mWordIterator = new WordIterator();
-        }
-
-        final int windowStart = Math.max(0, start - WORD_ITERATOR_WINDOW_WIDTH);
-        final int windowEnd = Math.min(mText.length(), end + WORD_ITERATOR_WINDOW_WIDTH);
-        mWordIterator.setCharSequence(mText.subSequence(windowStart, windowEnd));
-
-        return windowStart;
-    }
-
     private SpellChecker getSpellChecker() {
         if (mSpellChecker == null) {
             mSpellChecker = new SpellChecker(this);