From 7e821f0539147b9e8959c4d5c5e39304a4946be0 Mon Sep 17 00:00:00 2001 From: Qi Wang Date: Thu, 18 Mar 2021 15:57:46 +0800 Subject: [PATCH] Fix the SuggestionSpan overwrite logic. Previously, the SpellChecker use the start and end positions as the key to maintain the SuggestionSpan cache. However, the actual start/end position of the SuggestionSpan may changes when the text is changed. Hence we may not be able to find the corresponding SuggestonSpan for a new suggestion with the changed start/end positions. In this fix, we deprecated the cache and use Spanned#getSpans to find overlapped spans instead. Change-Id: I1e732f335ad745d85a7620e3ced6858551f18232 Fix: 182982338 Test: atest CtsInputMethodTestCases:SpellCheckerTest Change-Id: I327e5163499cfc978ff8480664a7e728b6ea7249 --- core/java/android/widget/SpellChecker.java | 58 ++++++++++++---------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/core/java/android/widget/SpellChecker.java b/core/java/android/widget/SpellChecker.java index a63305e55e538..287c18275a96b 100644 --- a/core/java/android/widget/SpellChecker.java +++ b/core/java/android/widget/SpellChecker.java @@ -20,12 +20,10 @@ import android.annotation.Nullable; import android.text.Editable; import android.text.Selection; import android.text.Spanned; -import android.text.TextUtils; import android.text.method.WordIterator; import android.text.style.SpellCheckSpan; import android.text.style.SuggestionSpan; import android.util.Log; -import android.util.LruCache; import android.util.Range; import android.view.textservice.SentenceSuggestionsInfo; import android.view.textservice.SpellCheckerSession; @@ -98,10 +96,6 @@ public class SpellChecker implements SpellCheckerSessionListener { private Runnable mSpellRunnable; - private static final int SUGGESTION_SPAN_CACHE_SIZE = 10; - private final LruCache mSuggestionSpanCache = - new LruCache(SUGGESTION_SPAN_CACHE_SIZE); - public SpellChecker(TextView textView) { mTextView = textView; @@ -144,7 +138,6 @@ public class SpellChecker implements SpellCheckerSessionListener { // Remove existing misspelled SuggestionSpans mTextView.removeMisspelledSpans((Editable) mTextView.getText()); - mSuggestionSpanCache.evictAll(); } private void setLocale(Locale locale) { @@ -410,16 +403,7 @@ public class SpellChecker implements SpellCheckerSessionListener { } if (spellCheckSpanStart >= 0 && spellCheckSpanEnd > spellCheckSpanStart && end > start) { - final Long key = Long.valueOf(TextUtils.packRangeInLong(start, end)); - final SuggestionSpan tempSuggestionSpan = mSuggestionSpanCache.get(key); - if (tempSuggestionSpan != null) { - if (DBG) { - Log.i(TAG, "Remove existing misspelled span. " - + editable.subSequence(start, end)); - } - editable.removeSpan(tempSuggestionSpan); - mSuggestionSpanCache.remove(key); - } + removeErrorSuggestionSpan(editable, start, end, RemoveReason.OBSOLETE); } } return spellCheckSpan; @@ -428,6 +412,35 @@ public class SpellChecker implements SpellCheckerSessionListener { return null; } + private enum RemoveReason { + /** + * Indicates the previous SuggestionSpan is replaced by a new SuggestionSpan. + */ + REPLACE, + /** + * Indicates the previous SuggestionSpan is removed because corresponding text is + * considered as valid words now. + */ + OBSOLETE, + } + + private static void removeErrorSuggestionSpan( + Editable editable, int start, int end, RemoveReason reason) { + SuggestionSpan[] spans = editable.getSpans(start, end, SuggestionSpan.class); + for (SuggestionSpan span : spans) { + if (editable.getSpanStart(span) == start + && editable.getSpanEnd(span) == end + && (span.getFlags() & (SuggestionSpan.FLAG_MISSPELLED + | SuggestionSpan.FLAG_GRAMMAR_ERROR)) != 0) { + if (DBG) { + Log.i(TAG, "Remove existing misspelled/grammar error span on " + + editable.subSequence(start, end) + ", reason: " + reason); + } + editable.removeSpan(span); + } + } + } + @Override public void onGetSuggestions(SuggestionsInfo[] results) { final Editable editable = (Editable) mTextView.getText(); @@ -543,16 +556,7 @@ public class SpellChecker implements SpellCheckerSessionListener { } SuggestionSpan suggestionSpan = new SuggestionSpan(mTextView.getContext(), suggestions, flags); - final Long key = Long.valueOf(TextUtils.packRangeInLong(start, end)); - final SuggestionSpan tempSuggestionSpan = mSuggestionSpanCache.get(key); - if (tempSuggestionSpan != null) { - if (DBG) { - Log.i(TAG, "Cached span on the same position is cleard. " - + editable.subSequence(start, end)); - } - editable.removeSpan(tempSuggestionSpan); - } - mSuggestionSpanCache.put(key, suggestionSpan); + removeErrorSuggestionSpan(editable, start, end, RemoveReason.REPLACE); editable.setSpan(suggestionSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); mTextView.invalidateRegion(start, end, false /* No cursor involved */);