Bug 5384674: When only one suggestion is returned, it is displayed twice

Two separate issues here:
- The results of the spell checker may be identical to the one set by the IME. Since
we merge the spans, the entries are duplicated. Filter spell checker results to avoid
these duplicates.

- When the text is saved on rotation, the spans are saved and restored. Since we start
a new spell check when the window is attached, it also doubles the size.

Change-Id: I21e1a5ae1b264bc97f44d762e4589bf520c6c19c
diff --git a/core/java/android/widget/SpellChecker.java b/core/java/android/widget/SpellChecker.java
index ac9535a..772c129 100644
--- a/core/java/android/widget/SpellChecker.java
+++ b/core/java/android/widget/SpellChecker.java
@@ -30,8 +30,6 @@
 
 import com.android.internal.util.ArrayUtils;
 
-import java.util.Locale;
-
 
 /**
  * Helper class for TextView. Bridge between the TextView and the Dictionnary service.
@@ -167,15 +165,12 @@
 
     @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;
             final int sequenceNumber = suggestionsInfo.getSequence();
 
             for (int j = 0; j < mLength; j++) {
-                final SpellCheckSpan spellCheckSpan = mSpellCheckSpans[j];
-
                 if (sequenceNumber == mIds[j]) {
                     final int attributes = suggestionsInfo.getSuggestionsAttributes();
                     boolean isInDictionary =
@@ -184,31 +179,78 @@
                             ((attributes & SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO) > 0);
 
                     if (!isInDictionary && looksLikeTypo) {
-                        String[] suggestions = getSuggestions(suggestionsInfo);
-                        SuggestionSpan suggestionSpan = new SuggestionSpan(
-                                mTextView.getContext(), suggestions,
-                                SuggestionSpan.FLAG_EASY_CORRECT |
-                                SuggestionSpan.FLAG_MISSPELLED);
-                        final int start = editable.getSpanStart(spellCheckSpan);
-                        final int end = editable.getSpanEnd(spellCheckSpan);
-                        editable.setSpan(suggestionSpan, start, end,
-                                Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-                        // TODO limit to the word rectangle region
-                        mTextView.invalidate();
+                        createMisspelledSuggestionSpan(suggestionsInfo, mSpellCheckSpans[j]);
                     }
-                    editable.removeSpan(spellCheckSpan);
+                    break;
                 }
             }
         }
     }
 
-    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++) {
-            suggestions[j] = suggestionsInfo.getSuggestionAt(j);
+    private void createMisspelledSuggestionSpan(SuggestionsInfo suggestionsInfo,
+            SpellCheckSpan spellCheckSpan) {
+        final Editable editable = (Editable) mTextView.getText();
+        final int start = editable.getSpanStart(spellCheckSpan);
+        final int end = editable.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);
+        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]);
+            if (spanStart != start || spanEnd != end) {
+                suggestionSpans[i] = null;
+                break;
+            }
         }
-        return suggestions;
+
+        final int suggestionsCount = suggestionsInfo.getSuggestionsCount();
+        String[] suggestions;
+        if (suggestionsCount <= 0) {
+            // A negative suggestion count is possible
+            suggestions = ArrayUtils.emptyArray(String.class);
+        } else {
+            int numberOfSuggestions = 0;
+            suggestions = new String[suggestionsCount];
+
+            for (int i = 0; i < suggestionsCount; i++) {
+                final String spellSuggestion = suggestionsInfo.getSuggestionAt(i);
+                if (spellSuggestion == null) break;
+                boolean suggestionFound = false;
+
+                for (int j = 0; j < length && !suggestionFound; j++) {
+                    if (suggestionSpans[j] == null) break;
+
+                    String[] suggests = suggestionSpans[j].getSuggestions();
+                    for (int k = 0; k < suggests.length; k++) {
+                        if (spellSuggestion.equals(suggests[k])) {
+                            // The suggestion is already provided by an other SuggestionSpan
+                            suggestionFound = true;
+                            break;
+                        }
+                    }
+                }
+
+                if (!suggestionFound) {
+                    suggestions[numberOfSuggestions++] = spellSuggestion;
+                }
+            }
+
+            if (numberOfSuggestions != suggestionsCount) {
+                String[] newSuggestions = new String[numberOfSuggestions];
+                System.arraycopy(suggestions, 0, newSuggestions, 0, numberOfSuggestions);
+                suggestions = newSuggestions;
+            }
+        }
+
+        SuggestionSpan suggestionSpan = new SuggestionSpan(mTextView.getContext(), suggestions,
+                SuggestionSpan.FLAG_EASY_CORRECT | SuggestionSpan.FLAG_MISSPELLED);
+        editable.setSpan(suggestionSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+        // TODO limit to the word rectangle region
+        mTextView.invalidate();
+        editable.removeSpan(spellCheckSpan);
     }
 }
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 5cd7902..052cbbc 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -353,6 +353,8 @@
     // 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;
@@ -2937,11 +2939,19 @@
 
                 Spannable sp = new SpannableString(mText);
 
-                for (ChangeWatcher cw :
-                     sp.getSpans(0, sp.length(), ChangeWatcher.class)) {
+                for (ChangeWatcher cw : sp.getSpans(0, sp.length(), ChangeWatcher.class)) {
                     sp.removeSpan(cw);
                 }
 
+                SuggestionSpan[] suggestionSpans = sp.getSpans(0, sp.length(), SuggestionSpan.class);
+                for (int i = 0; i < suggestionSpans.length; i++) {
+                    int flags = suggestionSpans[i].getFlags();
+                    if ((flags & SuggestionSpan.FLAG_EASY_CORRECT) != 0
+                            && (flags & SuggestionSpan.FLAG_MISSPELLED) != 0) {
+                        sp.removeSpan(suggestionSpans[i]);
+                    }
+                }
+
                 sp.removeSpan(mSuggestionRangeSpan);
 
                 ss.text = sp;
@@ -4449,7 +4459,6 @@
 
         if (mSpellChecker != null) {
             mSpellChecker.closeSession();
-            removeMisspelledSpans();
             // Forces the creation of a new SpellChecker next time this window is created.
             // Will handle the cases where the settings has been changed in the meantime.
             mSpellChecker = null;
@@ -8461,24 +8470,6 @@
         }
     }
 
-    /**
-     * Removes the suggestion spans for misspelled words.
-     */
-    private void removeMisspelledSpans() {
-        if (mText instanceof Spannable) {
-            Spannable spannable = (Spannable) mText;
-            SuggestionSpan[] suggestionSpans = spannable.getSpans(0,
-                    spannable.length(), SuggestionSpan.class);
-            for (int i = 0; i < suggestionSpans.length; i++) {
-                int flags = suggestionSpans[i].getFlags();
-                if ((flags & SuggestionSpan.FLAG_EASY_CORRECT) != 0
-                        && (flags & SuggestionSpan.FLAG_MISSPELLED) != 0) {
-                    spannable.removeSpan(suggestionSpans[i]);
-                }
-            }
-        }
-    }
-
     @Override
     public boolean onGenericMotionEvent(MotionEvent event) {
         if (mMovement != null && mText instanceof Spannable && mLayout != null) {
@@ -8959,9 +8950,8 @@
             mWordIterator = new WordIterator();
         }
 
-        final int TEXT_WINDOW_WIDTH = 50; // Should be larger than the longest word's length
-        final int windowStart = Math.max(0, start - TEXT_WINDOW_WIDTH);
-        final int windowEnd = Math.min(mText.length(), end + TEXT_WINDOW_WIDTH);
+        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;