Performance improvements for long text edition.

Limit each parse to batches of a few words, to keep the UI thread
responsive.

Possible optimizations for the future:
- SpellCheck in a thread, but that requires some locking mecanism
- Only spell check what is visible on screen. Will require additional
  spans to tag the pieces of text.

This is a cherry pick of 145656 into ICS-MR1

Patch Set 2: Make the Runnable shared and stop it when detached.

Change-Id: Ibf8e98274bda84b7176aac181ff267fc1f1fa4cb
diff --git a/core/java/android/widget/SpellChecker.java b/core/java/android/widget/SpellChecker.java
index a7c808e..8f495c9 100644
--- a/core/java/android/widget/SpellChecker.java
+++ b/core/java/android/widget/SpellChecker.java
@@ -43,7 +43,18 @@
  */
 public class SpellChecker implements SpellCheckerSessionListener {
 
-    private final static int MAX_SPELL_BATCH_SIZE = 50;
+    // No more than this number of words will be parsed on each iteration to ensure a minimum
+    // lock of the UI thread
+    public static final int MAX_NUMBER_OF_WORDS = 50;
+
+    // Rough estimate, such that the word iterator interval usually does not need to be shifted
+    public static final int AVERAGE_WORD_LENGTH = 7;
+
+    // When parsing, use a character window of that size. Will be shifted if needed
+    public static final int WORD_ITERATOR_INTERVAL = AVERAGE_WORD_LENGTH * MAX_NUMBER_OF_WORDS;
+
+    // Pause between each spell check to keep the UI smooth
+    private final static int SPELL_PAUSE_DURATION = 400; // milliseconds
 
     private final TextView mTextView;
 
@@ -71,6 +82,8 @@
 
     private TextServicesManager mTextServicesManager;
 
+    private Runnable mSpellRunnable;
+
     public SpellChecker(TextView textView) {
         mTextView = textView;
 
@@ -141,6 +154,10 @@
         for (int i = 0; i < length; i++) {
             mSpellParsers[i].finish();
         }
+
+        if (mSpellRunnable != null) {
+            mTextView.removeCallbacks(mSpellRunnable);
+        }
     }
 
     private int nextSpellCheckSpanIndex() {
@@ -254,6 +271,7 @@
                 System.arraycopy(textInfos, 0, textInfosCopy, 0, textInfosCount);
                 textInfos = textInfosCopy;
             }
+
             mSpellCheckerSession.getSuggestions(textInfos, SuggestionSpan.SUGGESTIONS_MAX_SIZE,
                     false /* TODO Set sequentialWords to true for initial spell check */);
         }
@@ -288,13 +306,29 @@
             }
         }
 
-        final int length = mSpellParsers.length;
-        for (int i = 0; i < length; i++) {
-            final SpellParser spellParser = mSpellParsers[i];
-            if (!spellParser.isFinished()) {
-                spellParser.parse();
-            }
+        scheduleNewSpellCheck();
+    }
+
+    private void scheduleNewSpellCheck() {
+        if (mSpellRunnable == null) {
+            mSpellRunnable = new Runnable() {
+                @Override
+                public void run() {
+                    final int length = mSpellParsers.length;
+                    for (int i = 0; i < length; i++) {
+                        final SpellParser spellParser = mSpellParsers[i];
+                        if (!spellParser.isFinished()) {
+                            spellParser.parse();
+                            break; // run one spell parser at a time to bound running time
+                        }
+                    }
+                }
+            };
+        } else {
+            mTextView.removeCallbacks(mSpellRunnable);
         }
+
+        mTextView.postDelayed(mSpellRunnable, SPELL_PAUSE_DURATION);
     }
 
     private void createMisspelledSuggestionSpan(Editable editable, SuggestionsInfo suggestionsInfo,
@@ -383,7 +417,9 @@
             // Iterate over the newly added text and schedule new SpellCheckSpans
             final int start = editable.getSpanStart(mRange);
             final int end = editable.getSpanEnd(mRange);
-            mWordIterator.setCharSequence(editable, start, end);
+
+            int wordIteratorWindowEnd = Math.min(end, start + WORD_ITERATOR_INTERVAL);
+            mWordIterator.setCharSequence(editable, start, wordIteratorWindowEnd);
 
             // Move back to the beginning of the current word, if any
             int wordStart = mWordIterator.preceding(start);
@@ -408,11 +444,16 @@
             SuggestionSpan[] suggestionSpans = editable.getSpans(start - 1, end + 1,
                     SuggestionSpan.class);
 
-            int nbWordsChecked = 0;
+            int wordCount = 0;
             boolean scheduleOtherSpellCheck = false;
 
             while (wordStart <= end) {
                 if (wordEnd >= start && wordEnd > wordStart) {
+                    if (wordCount >= MAX_NUMBER_OF_WORDS) {
+                        scheduleOtherSpellCheck = true;
+                        break;
+                    }
+
                     // 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) {
@@ -448,17 +489,20 @@
                     }
 
                     if (createSpellCheckSpan) {
-                        if (nbWordsChecked == MAX_SPELL_BATCH_SIZE) {
-                            scheduleOtherSpellCheck = true;
-                            break;
-                        }
                         addSpellCheckSpan(editable, wordStart, wordEnd);
-                        nbWordsChecked++;
                     }
+                    wordCount++;
                 }
 
                 // iterate word by word
+                int originalWordEnd = wordEnd;
                 wordEnd = mWordIterator.following(wordEnd);
+                if ((wordIteratorWindowEnd < end) &&
+                        (wordEnd == BreakIterator.DONE || wordEnd >= wordIteratorWindowEnd)) {
+                    wordIteratorWindowEnd = Math.min(end, originalWordEnd + WORD_ITERATOR_INTERVAL);
+                    mWordIterator.setCharSequence(editable, originalWordEnd, wordIteratorWindowEnd);
+                    wordEnd = mWordIterator.following(originalWordEnd);
+                }
                 if (wordEnd == BreakIterator.DONE) break;
                 wordStart = mWordIterator.getBeginning(wordEnd);
                 if (wordStart == BreakIterator.DONE) {