FileDocCategorySizeDatePackage
SpellChecker.javaAPI DocAndroid 5.1 API34238Thu Mar 12 22:22:10 GMT 2015android.widget

SpellChecker

public class SpellChecker extends Object implements android.view.textservice.SpellCheckerSession.SpellCheckerSessionListener
Helper class for TextView. Bridge between the TextView and the Dictionary service.
hide

Fields Summary
private static final String
TAG
private static final boolean
DBG
public static final int
MAX_NUMBER_OF_WORDS
public static final int
AVERAGE_WORD_LENGTH
public static final int
WORD_ITERATOR_INTERVAL
private static final int
SPELL_PAUSE_DURATION
private static final int
MIN_SENTENCE_LENGTH
private static final int
USE_SPAN_RANGE
private final TextView
mTextView
android.view.textservice.SpellCheckerSession
mSpellCheckerSession
private boolean
mIsSentenceSpellCheckSupported
final int
mCookie
private int[]
mIds
private android.text.style.SpellCheckSpan[]
mSpellCheckSpans
private int
mLength
private SpellParser[]
mSpellParsers
private int
mSpanSequenceCounter
private Locale
mCurrentLocale
private android.text.method.WordIterator
mWordIterator
private android.view.textservice.TextServicesManager
mTextServicesManager
private Runnable
mSpellRunnable
private static final int
SUGGESTION_SPAN_CACHE_SIZE
private final android.util.LruCache
mSuggestionSpanCache
Constructors Summary
public SpellChecker(TextView textView)


       
        mTextView = textView;

        // Arbitrary: these arrays will automatically double their sizes on demand
        final int size = 1;
        mIds = ArrayUtils.newUnpaddedIntArray(size);
        mSpellCheckSpans = new SpellCheckSpan[mIds.length];

        setLocale(mTextView.getSpellCheckerLocale());

        mCookie = hashCode();
    
Methods Summary
private voidaddSpellCheckSpan(android.text.Editable editable, int start, int end)

        final int index = nextSpellCheckSpanIndex();
        SpellCheckSpan spellCheckSpan = mSpellCheckSpans[index];
        editable.setSpan(spellCheckSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        spellCheckSpan.setSpellCheckInProgress(false);
        mIds[index] = mSpanSequenceCounter++;
    
public voidcloseSession()

        if (mSpellCheckerSession != null) {
            mSpellCheckerSession.close();
        }

        final int length = mSpellParsers.length;
        for (int i = 0; i < length; i++) {
            mSpellParsers[i].stop();
        }

        if (mSpellRunnable != null) {
            mTextView.removeCallbacks(mSpellRunnable);
        }
    
private voidcreateMisspelledSuggestionSpan(android.text.Editable editable, android.view.textservice.SuggestionsInfo suggestionsInfo, android.text.style.SpellCheckSpan spellCheckSpan, int offset, int length)

        final int spellCheckSpanStart = editable.getSpanStart(spellCheckSpan);
        final int spellCheckSpanEnd = editable.getSpanEnd(spellCheckSpan);
        if (spellCheckSpanStart < 0 || spellCheckSpanEnd <= spellCheckSpanStart)
            return; // span was removed in the meantime

        final int start;
        final int end;
        if (offset != USE_SPAN_RANGE && length != USE_SPAN_RANGE) {
            start = spellCheckSpanStart + offset;
            end = start + length;
        } else {
            start = spellCheckSpanStart;
            end = spellCheckSpanEnd;
        }

        final int suggestionsCount = suggestionsInfo.getSuggestionsCount();
        String[] suggestions;
        if (suggestionsCount > 0) {
            suggestions = new String[suggestionsCount];
            for (int i = 0; i < suggestionsCount; i++) {
                suggestions[i] = suggestionsInfo.getSuggestionAt(i);
            }
        } else {
            suggestions = ArrayUtils.emptyArray(String.class);
        }

        SuggestionSpan suggestionSpan = new SuggestionSpan(mTextView.getContext(), suggestions,
                SuggestionSpan.FLAG_EASY_CORRECT | SuggestionSpan.FLAG_MISSPELLED);
        // TODO: Remove mIsSentenceSpellCheckSupported by extracting an interface
        // to share the logic of word level spell checker and sentence level spell checker
        if (mIsSentenceSpellCheckSupported) {
            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);
        }
        editable.setSpan(suggestionSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

        mTextView.invalidateRegion(start, end, false /* No cursor involved */);
    
public static booleanhaveWordBoundariesChanged(android.text.Editable editable, int start, int end, int spanStart, int spanEnd)

        final boolean haveWordBoundariesChanged;
        if (spanEnd != start && spanStart != end) {
            haveWordBoundariesChanged = true;
            if (DBG) {
                Log.d(TAG, "(1) Text inside the span has been modified. Remove.");
            }
        } else if (spanEnd == start && start < editable.length()) {
            final int codePoint = Character.codePointAt(editable, start);
            haveWordBoundariesChanged = Character.isLetterOrDigit(codePoint);
            if (DBG) {
                Log.d(TAG, "(2) Characters have been appended to the spanned text. "
                        + (haveWordBoundariesChanged ? "Remove.<" : "Keep. <") + (char)(codePoint)
                        + ">, " + editable + ", " + editable.subSequence(spanStart, spanEnd) + ", "
                        + start);
            }
        } else if (spanStart == end && end > 0) {
            final int codePoint = Character.codePointBefore(editable, end);
            haveWordBoundariesChanged = Character.isLetterOrDigit(codePoint);
            if (DBG) {
                Log.d(TAG, "(3) Characters have been prepended to the spanned text. "
                        + (haveWordBoundariesChanged ? "Remove.<" : "Keep.<") + (char)(codePoint)
                        + ">, " + editable + ", " + editable.subSequence(spanStart, spanEnd) + ", "
                        + end);
            }
        } else {
            if (DBG) {
                Log.d(TAG, "(4) Characters adjacent to the spanned text were deleted. Keep.");
            }
            haveWordBoundariesChanged = false;
        }
        return haveWordBoundariesChanged;
    
private booleanisSessionActive()

return
true if a spell checker session has successfully been created. Returns false if not, for instance when spell checking has been disabled in settings.

        return mSpellCheckerSession != null;
    
private intnextSpellCheckSpanIndex()

        for (int i = 0; i < mLength; i++) {
            if (mIds[i] < 0) return i;
        }

        mIds = GrowingArrayUtils.append(mIds, mLength, 0);
        mSpellCheckSpans = GrowingArrayUtils.append(
                mSpellCheckSpans, mLength, new SpellCheckSpan());
        mLength++;
        return mLength - 1;
    
public voidonGetSentenceSuggestions(android.view.textservice.SentenceSuggestionsInfo[] results)

        final Editable editable = (Editable) mTextView.getText();

        for (int i = 0; i < results.length; ++i) {
            final SentenceSuggestionsInfo ssi = results[i];
            if (ssi == null) {
                continue;
            }
            SpellCheckSpan spellCheckSpan = null;
            for (int j = 0; j < ssi.getSuggestionsCount(); ++j) {
                final SuggestionsInfo suggestionsInfo = ssi.getSuggestionsInfoAt(j);
                if (suggestionsInfo == null) {
                    continue;
                }
                final int offset = ssi.getOffsetAt(j);
                final int length = ssi.getLengthAt(j);
                final SpellCheckSpan scs = onGetSuggestionsInternal(
                        suggestionsInfo, offset, length);
                if (spellCheckSpan == null && scs != null) {
                    // the spellCheckSpan is shared by all the "SuggestionsInfo"s in the same
                    // SentenceSuggestionsInfo. Removal is deferred after this loop.
                    spellCheckSpan = scs;
                }
            }
            if (spellCheckSpan != null) {
                // onSpellCheckSpanRemoved will recycle this span in the pool
                editable.removeSpan(spellCheckSpan);
            }
        }
        scheduleNewSpellCheck();
    
public voidonGetSuggestions(android.view.textservice.SuggestionsInfo[] results)

        final Editable editable = (Editable) mTextView.getText();
        for (int i = 0; i < results.length; ++i) {
            final SpellCheckSpan spellCheckSpan =
                    onGetSuggestionsInternal(results[i], USE_SPAN_RANGE, USE_SPAN_RANGE);
            if (spellCheckSpan != null) {
                // onSpellCheckSpanRemoved will recycle this span in the pool
                editable.removeSpan(spellCheckSpan);
            }
        }
        scheduleNewSpellCheck();
    
private android.text.style.SpellCheckSpanonGetSuggestionsInternal(android.view.textservice.SuggestionsInfo suggestionsInfo, int offset, int length)

        if (suggestionsInfo == null || suggestionsInfo.getCookie() != mCookie) {
            return null;
        }
        final Editable editable = (Editable) mTextView.getText();
        final int sequenceNumber = suggestionsInfo.getSequence();
        for (int k = 0; k < mLength; ++k) {
            if (sequenceNumber == mIds[k]) {
                final int attributes = suggestionsInfo.getSuggestionsAttributes();
                final boolean isInDictionary =
                        ((attributes & SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY) > 0);
                final boolean looksLikeTypo =
                        ((attributes & SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO) > 0);

                final SpellCheckSpan spellCheckSpan = mSpellCheckSpans[k];
                //TODO: we need to change that rule for results from a sentence-level spell
                // checker that will probably be in dictionary.
                if (!isInDictionary && looksLikeTypo) {
                    createMisspelledSuggestionSpan(
                            editable, suggestionsInfo, spellCheckSpan, offset, length);
                } else {
                    // Valid word -- isInDictionary || !looksLikeTypo
                    if (mIsSentenceSpellCheckSupported) {
                        // Allow the spell checker to remove existing misspelled span by
                        // overwriting the span over the same place
                        final int spellCheckSpanStart = editable.getSpanStart(spellCheckSpan);
                        final int spellCheckSpanEnd = editable.getSpanEnd(spellCheckSpan);
                        final int start;
                        final int end;
                        if (offset != USE_SPAN_RANGE && length != USE_SPAN_RANGE) {
                            start = spellCheckSpanStart + offset;
                            end = start + length;
                        } else {
                            start = spellCheckSpanStart;
                            end = spellCheckSpanEnd;
                        }
                        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);
                            }
                        }
                    }
                }
                return spellCheckSpan;
            }
        }
        return null;
    
public voidonSelectionChanged()

        spellCheck();
    
public voidonSpellCheckSpanRemoved(android.text.style.SpellCheckSpan spellCheckSpan)

        // Recycle any removed SpellCheckSpan (from this code or during text edition)
        for (int i = 0; i < mLength; i++) {
            if (mSpellCheckSpans[i] == spellCheckSpan) {
                mIds[i] = -1;
                return;
            }
        }
    
private voidresetSession()

        closeSession();

        mTextServicesManager = (TextServicesManager) mTextView.getContext().
                getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE);
        if (!mTextServicesManager.isSpellCheckerEnabled()
                || mCurrentLocale == null
                || mTextServicesManager.getCurrentSpellCheckerSubtype(true) == null) {
            mSpellCheckerSession = null;
        } else {
            mSpellCheckerSession = mTextServicesManager.newSpellCheckerSession(
                    null /* Bundle not currently used by the textServicesManager */,
                    mCurrentLocale, this,
                    false /* means any available languages from current spell checker */);
            mIsSentenceSpellCheckSupported = true;
        }

        // Restore SpellCheckSpans in pool
        for (int i = 0; i < mLength; i++) {
            mIds[i] = -1;
        }
        mLength = 0;

        // Remove existing misspelled SuggestionSpans
        mTextView.removeMisspelledSpans((Editable) mTextView.getText());
        mSuggestionSpanCache.evictAll();
    
private voidscheduleNewSpellCheck()

        if (DBG) {
            Log.i(TAG, "schedule new spell check.");
        }
        if (mSpellRunnable == null) {
            mSpellRunnable = new Runnable() {
                @Override
                public void run() {
                    final int length = mSpellParsers.length;
                    for (int i = 0; i < length; i++) {
                        final SpellParser spellParser = mSpellParsers[i];
                        if (!spellParser.isFinished()) {
                            spellParser.parse();
                            break; // run one spell parser at a time to bound running time
                        }
                    }
                }
            };
        } else {
            mTextView.removeCallbacks(mSpellRunnable);
        }

        mTextView.postDelayed(mSpellRunnable, SPELL_PAUSE_DURATION);
    
private voidsetLocale(java.util.Locale locale)

        mCurrentLocale = locale;

        resetSession();

        if (locale != null) {
            // Change SpellParsers' wordIterator locale
            mWordIterator = new WordIterator(locale);
        }

        // This class is the listener for locale change: warn other locale-aware objects
        mTextView.onLocaleChanged();
    
public voidspellCheck(int start, int end)

        if (DBG) {
            Log.d(TAG, "Start spell-checking: " + start + ", " + end);
        }
        final Locale locale = mTextView.getSpellCheckerLocale();
        final boolean isSessionActive = isSessionActive();
        if (locale == null || mCurrentLocale == null || (!(mCurrentLocale.equals(locale)))) {
            setLocale(locale);
            // Re-check the entire text
            start = 0;
            end = mTextView.getText().length();
        } else {
            final boolean spellCheckerActivated = mTextServicesManager.isSpellCheckerEnabled();
            if (isSessionActive != spellCheckerActivated) {
                // Spell checker has been turned of or off since last spellCheck
                resetSession();
            }
        }

        if (!isSessionActive) return;

        // Find first available SpellParser from pool
        final int length = mSpellParsers.length;
        for (int i = 0; i < length; i++) {
            final SpellParser spellParser = mSpellParsers[i];
            if (spellParser.isFinished()) {
                spellParser.parse(start, end);
                return;
            }
        }

        if (DBG) {
            Log.d(TAG, "new spell parser.");
        }
        // No available parser found in pool, create a new one
        SpellParser[] newSpellParsers = new SpellParser[length + 1];
        System.arraycopy(mSpellParsers, 0, newSpellParsers, 0, length);
        mSpellParsers = newSpellParsers;

        SpellParser spellParser = new SpellParser();
        mSpellParsers[length] = spellParser;
        spellParser.parse(start, end);
    
private voidspellCheck()

        if (mSpellCheckerSession == null) return;

        Editable editable = (Editable) mTextView.getText();
        final int selectionStart = Selection.getSelectionStart(editable);
        final int selectionEnd = Selection.getSelectionEnd(editable);

        TextInfo[] textInfos = new TextInfo[mLength];
        int textInfosCount = 0;

        for (int i = 0; i < mLength; i++) {
            final SpellCheckSpan spellCheckSpan = mSpellCheckSpans[i];
            if (mIds[i] < 0 || spellCheckSpan.isSpellCheckInProgress()) continue;

            final int start = editable.getSpanStart(spellCheckSpan);
            final int end = editable.getSpanEnd(spellCheckSpan);

            // Do not check this word if the user is currently editing it
            final boolean isEditing;

            // Defer spell check when typing a word with an interior apostrophe.
            // TODO: a better solution to this would be to make the word
            // iterator locale-sensitive and include the apostrophe in
            // languages that use it (such as English).
            final boolean apostrophe = (selectionStart == end + 1 && editable.charAt(end) == '\'");
            if (mIsSentenceSpellCheckSupported) {
                // Allow the overlap of the cursor and the first boundary of the spell check span
                // no to skip the spell check of the following word because the
                // following word will never be spell-checked even if the user finishes composing
                isEditing = !apostrophe && (selectionEnd <= start || selectionStart > end);
            } else {
                isEditing = !apostrophe && (selectionEnd < start || selectionStart > end);
            }
            if (start >= 0 && end > start && isEditing) {
                spellCheckSpan.setSpellCheckInProgress(true);
                final TextInfo textInfo = new TextInfo(editable, start, end, mCookie, mIds[i]);
                textInfos[textInfosCount++] = textInfo;
                if (DBG) {
                    Log.d(TAG, "create TextInfo: (" + i + "/" + mLength + ") text = "
                            + textInfo.getSequence() + ", cookie = " + mCookie + ", seq = "
                            + mIds[i] + ", sel start = " + selectionStart + ", sel end = "
                            + selectionEnd + ", start = " + start + ", end = " + end);
                }
            }
        }

        if (textInfosCount > 0) {
            if (textInfosCount < textInfos.length) {
                TextInfo[] textInfosCopy = new TextInfo[textInfosCount];
                System.arraycopy(textInfos, 0, textInfosCopy, 0, textInfosCount);
                textInfos = textInfosCopy;
            }

            if (mIsSentenceSpellCheckSupported) {
                mSpellCheckerSession.getSentenceSuggestions(
                        textInfos, SuggestionSpan.SUGGESTIONS_MAX_SIZE);
            } else {
                mSpellCheckerSession.getSuggestions(textInfos, SuggestionSpan.SUGGESTIONS_MAX_SIZE,
                        false /* TODO Set sequentialWords to true for initial spell check */);
            }
        }