Merge "Too many SpellCheckSpans are created."
diff --git a/core/java/android/text/SpannableStringBuilder.java b/core/java/android/text/SpannableStringBuilder.java
index fdbec20..231f913 100644
--- a/core/java/android/text/SpannableStringBuilder.java
+++ b/core/java/android/text/SpannableStringBuilder.java
@@ -710,18 +710,17 @@
 
         for (int i = 0; i < spanCount; i++) {
             int spanStart = starts[i];
-            int spanEnd = ends[i];
-
             if (spanStart > gapstart) {
                 spanStart -= gaplen;
             }
-            if (spanEnd > gapstart) {
-                spanEnd -= gaplen;
-            }
-
             if (spanStart > queryEnd) {
                 continue;
             }
+
+            int spanEnd = ends[i];
+            if (spanEnd > gapstart) {
+                spanEnd -= gaplen;
+            }
             if (spanEnd < queryStart) {
                 continue;
             }
diff --git a/core/java/android/text/style/SpellCheckSpan.java b/core/java/android/text/style/SpellCheckSpan.java
index caaae99..0d8a103 100644
--- a/core/java/android/text/style/SpellCheckSpan.java
+++ b/core/java/android/text/style/SpellCheckSpan.java
@@ -39,8 +39,8 @@
         mSpellCheckInProgress = (src.readInt() != 0);
     }
 
-    public void setSpellCheckInProgress() {
-        mSpellCheckInProgress = true;
+    public void setSpellCheckInProgress(boolean inProgress) {
+        mSpellCheckInProgress = inProgress;
     }
 
     public boolean isSpellCheckInProgress() {
diff --git a/core/java/android/widget/SpellChecker.java b/core/java/android/widget/SpellChecker.java
index 6b2f3e4..5d8db2f 100644
--- a/core/java/android/widget/SpellChecker.java
+++ b/core/java/android/widget/SpellChecker.java
@@ -22,7 +22,6 @@
 import android.text.Spanned;
 import android.text.style.SpellCheckSpan;
 import android.text.style.SuggestionSpan;
-import android.util.Log;
 import android.view.textservice.SpellCheckerSession;
 import android.view.textservice.SpellCheckerSession.SpellCheckerSessionListener;
 import android.view.textservice.SuggestionsInfo;
@@ -40,23 +39,21 @@
  * @hide
  */
 public class SpellChecker implements SpellCheckerSessionListener {
-    private static final String LOG_TAG = "SpellChecker";
-    private static final boolean DEBUG_SPELL_CHECK = false;
-    private static final int DELAY_BEFORE_SPELL_CHECK = 400; // milliseconds
 
     private final TextView mTextView;
 
     final SpellCheckerSession mSpellCheckerSession;
     final int mCookie;
 
-    // Paired arrays for the (id, spellCheckSpan) pair. mIndex is the next available position
+    // 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.
     private int[] mIds;
     private SpellCheckSpan[] mSpellCheckSpans;
-    // The actual current number of used slots in the above arrays
+    // The mLength first elements of the above arrays have been initialized
     private int mLength;
 
     private int mSpanSequenceCounter = 0;
-    private Runnable mChecker;
 
     public SpellChecker(TextView textView) {
         mTextView = textView;
@@ -69,7 +66,7 @@
         mCookie = hashCode();
 
         // Arbitrary: 4 simultaneous spell check spans. Will automatically double size on demand
-        final int size = ArrayUtils.idealObjectArraySize(4);
+        final int size = ArrayUtils.idealObjectArraySize(1);
         mIds = new int[size];
         mSpellCheckSpans = new SpellCheckSpan[size];
         mLength = 0;
@@ -89,73 +86,50 @@
         }
     }
 
-    public void addSpellCheckSpan(SpellCheckSpan spellCheckSpan) {
-        int length = mIds.length;
-        if (mLength >= length) {
-            final int newSize = length * 2;
+    private int nextSpellCheckSpanIndex() {
+        for (int i = 0; i < mLength; i++) {
+            if (mIds[i] < 0) return i;
+        }
+
+        if (mLength == mSpellCheckSpans.length) {
+            final int newSize = mLength * 2;
             int[] newIds = new int[newSize];
             SpellCheckSpan[] newSpellCheckSpans = new SpellCheckSpan[newSize];
-            System.arraycopy(mIds, 0, newIds, 0, length);
-            System.arraycopy(mSpellCheckSpans, 0, newSpellCheckSpans, 0, length);
+            System.arraycopy(mIds, 0, newIds, 0, mLength);
+            System.arraycopy(mSpellCheckSpans, 0, newSpellCheckSpans, 0, mLength);
             mIds = newIds;
             mSpellCheckSpans = newSpellCheckSpans;
         }
 
-        mIds[mLength] = mSpanSequenceCounter++;
-        mSpellCheckSpans[mLength] = spellCheckSpan;
+        mSpellCheckSpans[mLength] = new SpellCheckSpan();
         mLength++;
+        return mLength - 1;
+    }
 
-        if (DEBUG_SPELL_CHECK) {
-            final Editable mText = (Editable) mTextView.getText();
-            int start = mText.getSpanStart(spellCheckSpan);
-            int end = mText.getSpanEnd(spellCheckSpan);
-            if (start >= 0 && end >= 0) {
-                Log.d(LOG_TAG, "Schedule check " + mText.subSequence(start, end));
-            } else {
-                Log.d(LOG_TAG, "Schedule check   EMPTY!");
-            }
-        }
-
-        scheduleSpellCheck();
+    public void addSpellCheckSpan(int wordStart, int wordEnd) {
+        final int index = nextSpellCheckSpanIndex();
+        ((Editable) mTextView.getText()).setSpan(mSpellCheckSpans[index], wordStart, wordEnd,
+                Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+        mIds[index] = mSpanSequenceCounter++;
     }
 
     public void removeSpellCheckSpan(SpellCheckSpan spellCheckSpan) {
         for (int i = 0; i < mLength; i++) {
             if (mSpellCheckSpans[i] == spellCheckSpan) {
-                removeAtIndex(i);
+                mSpellCheckSpans[i].setSpellCheckInProgress(false);
+                mIds[i] = -1;
                 return;
             }
         }
     }
 
-    private void removeAtIndex(int i) {
-        System.arraycopy(mIds, i + 1, mIds, i, mLength - i - 1);
-        System.arraycopy(mSpellCheckSpans, i + 1, mSpellCheckSpans, i, mLength - i - 1);
-        mLength--;
-    }
-
     public void onSelectionChanged() {
-        scheduleSpellCheck();
+        spellCheck();
     }
 
-    private void scheduleSpellCheck() {
-        if (mLength == 0) return;
+    public void spellCheck() {
         if (mSpellCheckerSession == null) return;
 
-        if (mChecker != null) {
-            mTextView.removeCallbacks(mChecker);
-        }
-        if (mChecker == null) {
-            mChecker = new Runnable() {
-                public void run() {
-                  spellCheck();
-                }
-            };
-        }
-        mTextView.postDelayed(mChecker, DELAY_BEFORE_SPELL_CHECK);
-    }
-
-    private void spellCheck() {
         final Editable editable = (Editable) mTextView.getText();
         final int selectionStart = Selection.getSelectionStart(editable);
         final int selectionEnd = Selection.getSelectionEnd(editable);
@@ -164,8 +138,7 @@
         int textInfosCount = 0;
 
         for (int i = 0; i < mLength; i++) {
-            SpellCheckSpan spellCheckSpan = mSpellCheckSpans[i];
-
+            final SpellCheckSpan spellCheckSpan = mSpellCheckSpans[i];
             if (spellCheckSpan.isSpellCheckInProgress()) continue;
 
             final int start = editable.getSpanStart(spellCheckSpan);
@@ -174,7 +147,7 @@
             // 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();
-                spellCheckSpan.setSpellCheckInProgress();
+                spellCheckSpan.setSpellCheckInProgress(true);
                 textInfos[textInfosCount++] = new TextInfo(word, mCookie, mIds[i]);
             }
         }
@@ -196,27 +169,18 @@
         for (int i = 0; i < results.length; i++) {
             SuggestionsInfo suggestionsInfo = results[i];
             if (suggestionsInfo.getCookie() != mCookie) continue;
-
             final int sequenceNumber = suggestionsInfo.getSequence();
-            // Starting from the end, to limit the number of array copy while removing
-            for (int j = mLength - 1; j >= 0; j--) {
+
+            for (int j = 0; j < mLength; j++) {
+                final SpellCheckSpan spellCheckSpan = mSpellCheckSpans[j];
+
                 if (sequenceNumber == mIds[j]) {
-                    SpellCheckSpan spellCheckSpan = mSpellCheckSpans[j];
                     final int attributes = suggestionsInfo.getSuggestionsAttributes();
                     boolean isInDictionary =
                             ((attributes & SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY) > 0);
                     boolean looksLikeTypo =
                             ((attributes & SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO) > 0);
 
-                    if (DEBUG_SPELL_CHECK) {
-                        final int start = editable.getSpanStart(spellCheckSpan);
-                        final int end = editable.getSpanEnd(spellCheckSpan);
-                        Log.d(LOG_TAG, "Result sequence=" + suggestionsInfo.getSequence() + " " +
-                                editable.subSequence(start, end) +
-                                "\t" + (isInDictionary?"IN_DICT" : "NOT_DICT") +
-                                "\t" + (looksLikeTypo?"TYPO" : "NOT_TYPO"));
-                    }
-
                     if (!isInDictionary && looksLikeTypo) {
                         String[] suggestions = getSuggestions(suggestionsInfo);
                         if (suggestions.length > 0) {
@@ -230,13 +194,6 @@
                                     Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                             // TODO limit to the word rectangle region
                             mTextView.invalidate();
-
-                            if (DEBUG_SPELL_CHECK) {
-                                String suggestionsString = "";
-                                for (String s : suggestions) { suggestionsString += s + "|"; }
-                                Log.d(LOG_TAG, "  Suggestions for " + sequenceNumber + " " +
-                                    editable.subSequence(start, end)+ "  " + suggestionsString);
-                            }
                         }
                     }
                     editable.removeSpan(spellCheckSpan);
@@ -246,9 +203,10 @@
     }
 
     private static String[] getSuggestions(SuggestionsInfo suggestionsInfo) {
+        // A negative suggestion count is possible
         final int len = Math.max(0, suggestionsInfo.getSuggestionsCount());
         String[] suggestions = new String[len];
-        for (int j = 0; j < len; ++j) {
+        for (int j = 0; j < len; j++) {
             suggestions[j] = suggestionsInfo.getSuggestionAt(j);
         }
         return suggestions;
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index bffdadc..51236e3 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -5537,7 +5537,7 @@
     @Override public boolean onCheckIsTextEditor() {
         return mInputType != EditorInfo.TYPE_NULL;
     }
-    
+
     @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
         if (onCheckIsTextEditor() && isEnabled()) {
             if (mInputMethodState == null) {
@@ -7492,9 +7492,6 @@
      */
     protected void onSelectionChanged(int selStart, int selEnd) {
         sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED);
-        if (mSpellChecker != null) {
-            mSpellChecker.onSelectionChanged();
-        }
     }
 
     /**
@@ -7553,6 +7550,8 @@
         for (int i = 0; i < length; i++) {
             final int s = text.getSpanStart(spans[i]);
             final int e = text.getSpanEnd(spans[i]);
+            // Spans that are adjacent to the edited region will be handled in
+            // updateSpellCheckSpans. Result depends on what will be added (space or text)
             if (e == start || s == end) break;
             text.removeSpan(spans[i]);
         }
@@ -7735,12 +7734,8 @@
             }
         }
 
-        if (what instanceof SpellCheckSpan) {
-            if (newStart < 0) {
-                getSpellChecker().removeSpellCheckSpan((SpellCheckSpan) what);
-            } else if (oldStart < 0) {
-                getSpellChecker().addSpellCheckSpan((SpellCheckSpan) what);
-            }
+        if (newStart < 0 && what instanceof SpellCheckSpan) {
+            getSpellChecker().removeSpellCheckSpan((SpellCheckSpan) what);
         }
     }
 
@@ -7750,8 +7745,8 @@
     private void updateSpellCheckSpans(int start, int end) {
         if (!isTextEditable() || !isSuggestionsEnabled() || !getSpellChecker().isSessionActive())
             return;
-        Editable text = (Editable) mText;
 
+        Editable text = (Editable) mText;
         WordIterator wordIterator = getWordIterator();
         wordIterator.setCharSequence(text);
 
@@ -7770,57 +7765,75 @@
             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 <= end) {
             if (wordEnd >= start) {
-                // A word across the interval boundaries must remove boundary edition spans
+                // A new word has been created across the interval boundaries. Remove previous spans
                 if (wordStart < start && wordEnd > start) {
-                    removeEditionSpansAt(start, text);
+                    removeSpansAt(start, spellCheckSpans, text);
+                    removeSpansAt(start, suggestionSpans, text);
                 }
 
                 if (wordStart < end && wordEnd > end) {
-                    removeEditionSpansAt(end, text);
+                    removeSpansAt(end, spellCheckSpans, text);
+                    removeSpansAt(end, suggestionSpans, text);
                 }
 
                 // Do not create new boundary spans if they already exist
                 boolean createSpellCheckSpan = true;
                 if (wordEnd == start) {
-                    SpellCheckSpan[] spellCheckSpans = text.getSpans(start, start,
-                            SpellCheckSpan.class);
-                    if (spellCheckSpans.length > 0) createSpellCheckSpan = false;
+                    for (int i = 0; i < numberOfSpellCheckSpans; i++) {
+                        final int spanEnd = text.getSpanEnd(spellCheckSpans[i]);
+                        if (spanEnd == start) {
+                            createSpellCheckSpan = false;
+                            break;
+                        }
+                    }
                 }
 
                 if (wordStart == end) {
-                    SpellCheckSpan[] spellCheckSpans = text.getSpans(end, end,
-                            SpellCheckSpan.class);
-                    if (spellCheckSpans.length > 0) createSpellCheckSpan = false;
+                    for (int i = 0; i < numberOfSpellCheckSpans; i++) {
+                        final int spanStart = text.getSpanEnd(spellCheckSpans[i]);
+                        if (spanStart == end) {
+                            createSpellCheckSpan = false;
+                            break;
+                        }
+                    }
                 }
 
                 if (createSpellCheckSpan) {
-                    text.setSpan(new SpellCheckSpan(), wordStart, wordEnd,
-                            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+                    mSpellChecker.addSpellCheckSpan(wordStart, wordEnd);
                 }
             }
 
             // iterate word by word
             wordEnd = wordIterator.following(wordEnd);
-            if (wordEnd == BreakIterator.DONE) return;
+            if (wordEnd == BreakIterator.DONE) break;
             wordStart = wordIterator.getBeginning(wordEnd);
             if (wordStart == BreakIterator.DONE) {
                 Log.e(LOG_TAG, "Unable to find word beginning from " + wordEnd + "in " + mText);
-                return;
+                break;
             }
         }
+
+        mSpellChecker.spellCheck();
     }
 
-    private static void removeEditionSpansAt(int offset, Editable text) {
-        SuggestionSpan[] suggestionSpans = text.getSpans(offset, offset, SuggestionSpan.class);
-        for (int i = 0; i < suggestionSpans.length; i++) {
-            text.removeSpan(suggestionSpans[i]);
-        }
-        SpellCheckSpan[] spellCheckSpans = text.getSpans(offset, offset, SpellCheckSpan.class);
-        for (int i = 0; i < spellCheckSpans.length; i++) {
-            text.removeSpan(spellCheckSpans[i]);
+    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);
         }
     }
 
@@ -8381,6 +8394,10 @@
                 boolean selectAllGotFocus = mSelectAllOnFocus && didTouchFocusSelect();
                 hideControllers();
                 if (!selectAllGotFocus && mText.length() > 0) {
+                    if (mSpellChecker != null) {
+                        // When the cursor moves, the word that was typed may need spell check
+                        mSpellChecker.onSelectionChanged();
+                    }
                     if (isCursorInsideEasyCorrectionSpan()) {
                         showSuggestions();
                     } else if (hasInsertionController()) {