Bug 5250788: LatinIME slows down as amount of Text increases

Removed unnecessary CharSequenceIterator and made the WordIterator
work on String instead of CharSequence

Submit words to the spell checker by batches.

Refactored WordIterator to make it intrinsically local.

Change-Id: Ie9e30691985a130fa55cd052005ddb22a21761cb
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);