/** * Copies the spans from the region <code>start...end</code> in * <code>source</code> to the region * <code>destoff...destoff+end-start</code> in <code>dest</code>. * Spans in <code>source</code> that begin before <code>start</code> * or end after <code>end</code> but overlap this range are trimmed * as if they began at <code>start</code> or ended at <code>end</code>. * Only SuggestionSpans that don't have the SPAN_PARAGRAPH span are copied. * * This code is almost entirely taken from {@link TextUtils#copySpansFrom}, except for the * kind of span that is copied. * * @throws IndexOutOfBoundsException if any of the copied spans * are out of range in <code>dest</code>. */ public static void copyNonParagraphSuggestionSpansFrom(Spanned source, int start, int end, Spannable dest, int destoff) { Object[] spans = source.getSpans(start, end, SuggestionSpan.class); for (int i = 0; i < spans.length; i++) { int fl = source.getSpanFlags(spans[i]); // We don't care about the PARAGRAPH flag in LatinIME code. However, if this flag // is set, Spannable#setSpan will throw an exception unless the span is on the edge // of a word. But the spans have been split into two by the getText{Before,After}Cursor // methods, so after concatenation they may end in the middle of a word. // Since we don't use them, we can just remove them and avoid crashing. fl &= ~Spanned.SPAN_PARAGRAPH; int st = source.getSpanStart(spans[i]); int en = source.getSpanEnd(spans[i]); if (st < start) st = start; if (en > end) en = end; dest.setSpan(spans[i], st - start + destoff, en - start + destoff, fl); } }
/** * Removes the suggestion spans. */ CharSequence removeSuggestionSpans(CharSequence text) { if (text instanceof Spanned) { Spannable spannable; if (text instanceof Spannable) { spannable = (Spannable) text; } else { spannable = new SpannableString(text); text = spannable; } SuggestionSpan[] spans = spannable.getSpans(0, text.length(), SuggestionSpan.class); for (int i = 0; i < spans.length; i++) { spannable.removeSpan(spans[i]); } } return text; }
void removeAdjacentSuggestionSpans(final int pos) { if (!(mText instanceof Editable)) return; final Editable text = (Editable) mText; final SuggestionSpan[] spans = text.getSpans(pos, pos, SuggestionSpan.class); final int length = spans.length; for (int i = 0; i < length; i++) { final int spanStart = text.getSpanStart(spans[i]); final int spanEnd = text.getSpanEnd(spans[i]); if (spanEnd == pos || spanStart == pos) { if (SpellChecker.haveWordBoundariesChanged(text, pos, pos, spanStart, spanEnd)) { text.removeSpan(spans[i]); } } } }
/** * @see android.text.style.SuggestionSpan */ public static Span suggestion(@NonNull final Context context, @NonNull final String[] suggestions, final int flags) { return new Span(new SpanBuilder() { @Override public Object build() { return new SuggestionSpan(context, suggestions, flags); } }); }
/** * @see android.text.style.SuggestionSpan */ public static Span suggestion(@NonNull final Locale locale, @NonNull final String[] suggestions, final int flags) { return new Span(new SpanBuilder() { @Override public Object build() { return new SuggestionSpan(locale, suggestions, flags); } }); }
/** * @see android.text.style.SuggestionSpan */ public static Span suggestion(@NonNull final Context context, @NonNull final Locale locale, @NonNull final String[] suggestions, final int flags, @NonNull final Class<?> notificationTargetClass) { return new Span(new SpanBuilder() { @Override public Object build() { return new SuggestionSpan(context, locale, suggestions, flags, notificationTargetClass); } }); }
@UsedForTesting public static CharSequence getTextWithAutoCorrectionIndicatorUnderline( final Context context, final String text, @Nonnull final Locale locale) { if (TextUtils.isEmpty(text) || OBJ_FLAG_AUTO_CORRECTION == null) { return text; } final Spannable spannable = new SpannableString(text); final SuggestionSpan suggestionSpan = new SuggestionSpan(context, locale, new String[] {} /* suggestions */, OBJ_FLAG_AUTO_CORRECTION, null); spannable.setSpan(suggestionSpan, 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_COMPOSING); return spannable; }
@UsedForTesting public static CharSequence getTextWithSuggestionSpan(final Context context, final String pickedWord, final SuggestedWords suggestedWords, final Locale locale) { if (TextUtils.isEmpty(pickedWord) || suggestedWords.isEmpty() || suggestedWords.isPrediction() || suggestedWords.isPunctuationSuggestions()) { return pickedWord; } final ArrayList<String> suggestionsList = new ArrayList<>(); for (int i = 0; i < suggestedWords.size(); ++i) { if (suggestionsList.size() >= SuggestionSpan.SUGGESTIONS_MAX_SIZE) { break; } final SuggestedWordInfo info = suggestedWords.getInfo(i); if (info.isKindOf(SuggestedWordInfo.KIND_PREDICTION)) { continue; } final String word = suggestedWords.getWord(i); if (!TextUtils.equals(pickedWord, word)) { suggestionsList.add(word.toString()); } } final SuggestionSpan suggestionSpan = new SuggestionSpan(context, locale, suggestionsList.toArray(new String[suggestionsList.size()]), 0 /* flags */, null); final Spannable spannable = new SpannableString(pickedWord); spannable.setSpan(suggestionSpan, 0, pickedWord.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); return spannable; }
/** * Returns first {@link Locale} found in the given array of {@link SuggestionSpan}. * @param suggestionSpans the array of {@link SuggestionSpan} to be examined. * @return the first {@link Locale} found in {@code suggestionSpans}. {@code null} when not * found. */ @UsedForTesting @Nullable public static Locale findFirstLocaleFromSuggestionSpans( final SuggestionSpan[] suggestionSpans) { for (final SuggestionSpan suggestionSpan : suggestionSpans) { final String localeString = suggestionSpan.getLocale(); if (TextUtils.isEmpty(localeString)) { continue; } return LocaleUtils.constructLocaleFromString(localeString); } return null; }
void removeMisspelledSpans(Spannable spannable) { SuggestionSpan[] suggestionSpans = spannable.getSpans(0, spannable.length(), SuggestionSpan.class); for (int i = 0; i < suggestionSpans.length; i++) { int flags = suggestionSpans[i].getFlags(); if ((flags & SuggestionSpan.FLAG_EASY_CORRECT) != 0 && (flags & SuggestionSpan.FLAG_MISSPELLED) != 0) { spannable.removeSpan(suggestionSpans[i]); } } }
private void sendBeforeTextChanged(CharSequence text, int start, int before, int after) { if (mListeners != null) { final ArrayList<TextWatcher> list = mListeners; final int count = list.size(); for (int i = 0; i < count; i++) { list.get(i).beforeTextChanged(text, start, before, after); } } // The spans that are inside or intersect the modified region no longer make sense removeIntersectingNonAdjacentSpans(start, start + before, SpellCheckSpan.class); removeIntersectingNonAdjacentSpans(start, start + before, SuggestionSpan.class); }
@Override public void onReceive(Context context, Intent intent) { if (SuggestionSpan.ACTION_SUGGESTION_PICKED.equals(intent.getAction())) { if (DBG) { final String before = intent.getStringExtra( SuggestionSpan.SUGGESTION_SPAN_PICKED_BEFORE); final String after = intent.getStringExtra( SuggestionSpan.SUGGESTION_SPAN_PICKED_AFTER); Log.d(TAG, "Received notification picked: " + before + "," + after); } } }
public static CharSequence getTextWithAutoCorrectionIndicatorUnderline( final Context context, final String text) { if (TextUtils.isEmpty(text) || OBJ_FLAG_AUTO_CORRECTION == null) { return text; } final Spannable spannable = new SpannableString(text); final SuggestionSpan suggestionSpan = new SuggestionSpan(context, null /* locale */, new String[] {} /* suggestions */, (int)OBJ_FLAG_AUTO_CORRECTION, SuggestionSpanPickedNotificationReceiver.class); spannable.setSpan(suggestionSpan, 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_COMPOSING); return spannable; }
public static CharSequence getTextWithSuggestionSpan(final Context context, final String pickedWord, final SuggestedWords suggestedWords, final boolean dictionaryAvailable) { if (!dictionaryAvailable || TextUtils.isEmpty(pickedWord) || suggestedWords.isEmpty() || suggestedWords.mIsPrediction || suggestedWords.mIsPunctuationSuggestions) { return pickedWord; } final Spannable spannable = new SpannableString(pickedWord); final ArrayList<String> suggestionsList = CollectionUtils.newArrayList(); for (int i = 0; i < suggestedWords.size(); ++i) { if (suggestionsList.size() >= SuggestionSpan.SUGGESTIONS_MAX_SIZE) { break; } final String word = suggestedWords.getWord(i); if (!TextUtils.equals(pickedWord, word)) { suggestionsList.add(word.toString()); } } // TODO: We should avoid adding suggestion span candidates that came from the bigram // prediction. final SuggestionSpan suggestionSpan = new SuggestionSpan(context, null /* locale */, suggestionsList.toArray(new String[suggestionsList.size()]), 0 /* flags */, SuggestionSpanPickedNotificationReceiver.class); spannable.setSpan(suggestionSpan, 0, pickedWord.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); return spannable; }
/** * Gets the suggestion spans that are put squarely on the word, with the exact start * and end of the span matching the boundaries of the word. * @return the list of spans. */ public SuggestionSpan[] getSuggestionSpansAtWord() { if (!(mTextAtCursor instanceof Spanned && mWord instanceof Spanned)) { return new SuggestionSpan[0]; } final Spanned text = (Spanned)mTextAtCursor; // Note: it's fine to pass indices negative or greater than the length of the string // to the #getSpans() method. The reason we need to get from -1 to +1 is that, the // spans were cut at the cursor position, and #getSpans(start, end) does not return // spans that end at `start' or begin at `end'. Consider the following case: // this| is (The | symbolizes the cursor position // ---- --- // In this case, the cursor is in position 4, so the 0~7 span has been split into // a 0~4 part and a 4~7 part. // If we called #getSpans(0, 4) in this case, we would only get the part from 0 to 4 // of the span, and not the part from 4 to 7, so we would not realize the span actually // extends from 0 to 7. But if we call #getSpans(-1, 5) we'll get both the 0~4 and // the 4~7 spans and we can merge them accordingly. // Any span starting more than 1 char away from the word boundaries in any direction // does not touch the word, so we don't need to consider it. That's why requesting // -1 ~ +1 is enough. // Of course this is only relevant if the cursor is at one end of the word. If it's // in the middle, the -1 and +1 are not necessary, but they are harmless. final SuggestionSpan[] spans = text.getSpans(mWordAtCursorStartIndex - 1, mWordAtCursorEndIndex + 1, SuggestionSpan.class); int readIndex = 0; int writeIndex = 0; for (; readIndex < spans.length; ++readIndex) { final SuggestionSpan span = spans[readIndex]; // The span may be null, as we null them when we find duplicates. Cf a few lines // down. if (null == span) continue; // Tentative span start and end. This may be modified later if we realize the // same span is also applied to other parts of the string. int spanStart = text.getSpanStart(span); int spanEnd = text.getSpanEnd(span); for (int i = readIndex + 1; i < spans.length; ++i) { if (span.equals(spans[i])) { // We found the same span somewhere else. Read the new extent of this // span, and adjust our values accordingly. spanStart = Math.min(spanStart, text.getSpanStart(spans[i])); spanEnd = Math.max(spanEnd, text.getSpanEnd(spans[i])); // ...and mark the span as processed. spans[i] = null; } } if (spanStart == mWordAtCursorStartIndex && spanEnd == mWordAtCursorEndIndex) { // If the span does not start and stop here, ignore it. It probably extends // past the start or end of the word, as happens in missing space correction // or EasyEditSpans put by voice input. spans[writeIndex++] = spans[readIndex]; } } return writeIndex == readIndex ? spans : Arrays.copyOfRange(spans, 0, writeIndex); }
/** * Check if the cursor is touching a word. If so, restart suggestions on this word, else * do nothing. */ private void restartSuggestionsOnWordTouchedByCursor() { // HACK: We may want to special-case some apps that exhibit bad behavior in case of // recorrection. This is a temporary, stopgap measure that will be removed later. // TODO: remove this. if (mAppWorkAroundsUtils.isBrokenByRecorrection()) return; // If the cursor is not touching a word, or if there is a selection, return right away. if (mLastSelectionStart != mLastSelectionEnd) return; // If we don't know the cursor location, return. if (mLastSelectionStart < 0) return; if (!mConnection.isCursorTouchingWord(mSettings.getCurrent())) return; final Range range = mConnection.getWordRangeAtCursor(mSettings.getWordSeparators(), 0 /* additionalPrecedingWordsCount */); if (null == range) return; // Happens if we don't have an input connection at all // If for some strange reason (editor bug or so) we measure the text before the cursor as // longer than what the entire text is supposed to be, the safe thing to do is bail out. if (range.mCharsBefore > mLastSelectionStart) return; final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList(); final String typedWord = range.mWord.toString(); if (range.mWord instanceof SpannableString) { final SpannableString spannableString = (SpannableString)range.mWord; int i = 0; for (Object object : spannableString.getSpans(0, spannableString.length(), SuggestionSpan.class)) { SuggestionSpan span = (SuggestionSpan)object; for (String s : span.getSuggestions()) { ++i; if (!TextUtils.equals(s, typedWord)) { suggestions.add(new SuggestedWordInfo(s, SuggestionStripView.MAX_SUGGESTIONS - i, SuggestedWordInfo.KIND_RESUMED, Dictionary.TYPE_RESUMED)); } } } } mWordComposer.setComposingWord(typedWord, mKeyboardSwitcher.getKeyboard()); mWordComposer.setCursorPositionWithinWord(range.mCharsBefore); mConnection.setComposingRegion(mLastSelectionStart - range.mCharsBefore, mLastSelectionEnd + range.mCharsAfter); final SuggestedWords suggestedWords; if (suggestions.isEmpty()) { // We come here if there weren't any suggestion spans on this word. We will try to // compute suggestions for it instead. final SuggestedWords suggestedWordsIncludingTypedWord = getSuggestedWords(Suggest.SESSION_TYPING); if (suggestedWordsIncludingTypedWord.size() > 1) { // We were able to compute new suggestions for this word. // Remove the typed word, since we don't want to display it in this case. // The #getSuggestedWordsExcludingTypedWord() method sets willAutoCorrect to false. suggestedWords = suggestedWordsIncludingTypedWord.getSuggestedWordsExcludingTypedWord(); } else { // No saved suggestions, and we were unable to compute any good one either. // Rather than displaying an empty suggestion strip, we'll display the original // word alone in the middle. // Since there is only one word, willAutoCorrect is false. suggestedWords = suggestedWordsIncludingTypedWord; } } else { // We found suggestion spans in the word. We'll create the SuggestedWords out of // them, and make willAutoCorrect false. suggestedWords = new SuggestedWords(suggestions, true /* typedWordValid */, false /* willAutoCorrect */, false /* isPunctuationSuggestions */, false /* isObsoleteSuggestions */, false /* isPrediction */); } // Note that it's very important here that suggestedWords.mWillAutoCorrect is false. // We never want to auto-correct on a resumed suggestion. Please refer to the three // places above where suggestedWords is affected. We also need to reset // mIsAutoCorrectionIndicatorOn to avoid showSuggestionStrip touching the text to adapt it. // TODO: remove mIsAutoCorrectionIndicator on (see comment on definition) mIsAutoCorrectionIndicatorOn = false; showSuggestionStrip(suggestedWords, typedWord); }