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

Commit b062e81e authored by Gilles Debunne's avatar Gilles Debunne
Browse files

Too many SpellCheckSpans are created.

Removed the Runnable in SpellChecker, spell check is triggered at the end
of updateSpellCheckSpans instead of when a new SpellCheckSpan is created.

Cache the spans in updateSpellCheckSpans to limit the calls to getSpans.

When typing, every new letter in a word will create a SpellCheckSpan
(this is needed in case the user taps somewhere else on the screen)
The SpellCheckSpans are pooled in SpellChecker to limit unnecessary new
SpellCheckSpan creation.

Minor optimization on test order in getSpans to avoid some calculation.

Spell check is not started everytime the selection is changed (would be
triggered when the insertion handle is moved). Explicitely do that only
on tap.

Change-Id: Ibacf80dd4ba098494e0b5ba0e58a362782fc8f71
parent 4c088485
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()) {