Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f4314dff authored by Gilles Debunne's avatar Gilles Debunne Committed by Android (Google) Code Review
Browse files

Merge "Too many SpellCheckSpans are created."

parents 2452ad30 b062e81e
Loading
Loading
Loading
Loading
+5 −6
Original line number Diff line number Diff line
@@ -710,18 +710,17 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable

        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;
            }
+2 −2
Original line number Diff line number Diff line
@@ -39,8 +39,8 @@ public class SpellCheckSpan implements ParcelableSpan {
        mSpellCheckInProgress = (src.readInt() != 0);
    }

    public void setSpellCheckInProgress() {
        mSpellCheckInProgress = true;
    public void setSpellCheckInProgress(boolean inProgress) {
        mSpellCheckInProgress = inProgress;
    }

    public boolean isSpellCheckInProgress() {
+34 −76
Original line number Diff line number Diff line
@@ -22,7 +22,6 @@ import android.text.Selection;
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 @@ import java.util.Locale;
 * @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 @@ public class SpellChecker implements SpellCheckerSessionListener {
        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 class SpellChecker implements SpellCheckerSessionListener {
        }
    }

    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++;

        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!");
            }
        return mLength - 1;
    }

        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 @@ public class SpellChecker implements SpellCheckerSessionListener {
        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 @@ public class SpellChecker implements SpellCheckerSessionListener {
            // 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 @@ public class SpellChecker implements SpellCheckerSessionListener {
        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 @@ public class SpellChecker implements SpellCheckerSessionListener {
                                    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 @@ public class SpellChecker implements SpellCheckerSessionListener {
    }

    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;
+49 −32
Original line number Diff line number Diff line
@@ -7492,9 +7492,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
     */
    protected void onSelectionChanged(int selStart, int selEnd) {
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED);
        if (mSpellChecker != null) {
            mSpellChecker.onSelectionChanged();
        }
    }

    /**
@@ -7553,6 +7550,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
        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 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
            }
        }

        if (what instanceof SpellCheckSpan) {
            if (newStart < 0) {
        if (newStart < 0 && what instanceof SpellCheckSpan) {
            getSpellChecker().removeSpellCheckSpan((SpellCheckSpan) what);
            } else if (oldStart < 0) {
                getSpellChecker().addSpellCheckSpan((SpellCheckSpan) what);
            }
        }
    }

@@ -7750,8 +7745,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
    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 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
            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;
            }
        }

    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]);
        mSpellChecker.spellCheck();
    }
        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 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
                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()) {