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()) {