| /* |
| * Copyright (C) 2011 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.text.style; |
| |
| import android.annotation.ColorInt; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.annotation.UnsupportedAppUsage; |
| import android.content.Context; |
| import android.content.res.TypedArray; |
| import android.graphics.Color; |
| import android.os.Parcel; |
| import android.os.Parcelable; |
| import android.os.SystemClock; |
| import android.text.ParcelableSpan; |
| import android.text.TextPaint; |
| import android.text.TextUtils; |
| import android.util.Log; |
| import android.widget.TextView; |
| |
| import java.util.Arrays; |
| import java.util.Locale; |
| |
| /** |
| * Holds suggestion candidates for the text enclosed in this span. |
| * |
| * When such a span is edited in an EditText, double tapping on the text enclosed in this span will |
| * display a popup dialog listing suggestion replacement for that text. The user can then replace |
| * the original text by one of the suggestions. |
| * |
| * These spans should typically be created by the input method to provide correction and alternates |
| * for the text. |
| * |
| * @see TextView#isSuggestionsEnabled() |
| */ |
| public class SuggestionSpan extends CharacterStyle implements ParcelableSpan { |
| |
| private static final String TAG = "SuggestionSpan"; |
| |
| /** |
| * Sets this flag if the suggestions should be easily accessible with few interactions. |
| * This flag should be set for every suggestions that the user is likely to use. |
| */ |
| public static final int FLAG_EASY_CORRECT = 0x0001; |
| |
| /** |
| * Sets this flag if the suggestions apply to a misspelled word/text. This type of suggestion is |
| * rendered differently to highlight the error. |
| */ |
| public static final int FLAG_MISSPELLED = 0x0002; |
| |
| /** |
| * Sets this flag if the auto correction is about to be applied to a word/text |
| * that the user is typing/composing. This type of suggestion is rendered differently |
| * to indicate the auto correction is happening. |
| */ |
| public static final int FLAG_AUTO_CORRECTION = 0x0004; |
| |
| /** |
| * This action is deprecated in {@link android.os.Build.VERSION_CODES#Q}. |
| * |
| * @deprecated For IMEs to receive this kind of user interaction signals, implement IMEs' own |
| * suggestion picker UI instead of relying on {@link SuggestionSpan}. To retrieve |
| * bounding boxes for each character of the composing text, use |
| * {@link android.view.inputmethod.CursorAnchorInfo}. |
| */ |
| @Deprecated |
| public static final String ACTION_SUGGESTION_PICKED = "android.text.style.SUGGESTION_PICKED"; |
| |
| /** |
| * This is deprecated in {@link android.os.Build.VERSION_CODES#Q}. |
| * |
| * @deprecated See {@link #ACTION_SUGGESTION_PICKED}. |
| */ |
| @Deprecated |
| public static final String SUGGESTION_SPAN_PICKED_AFTER = "after"; |
| /** |
| * This is deprecated in {@link android.os.Build.VERSION_CODES#Q}. |
| * |
| * @deprecated See {@link #ACTION_SUGGESTION_PICKED}. |
| */ |
| @Deprecated |
| public static final String SUGGESTION_SPAN_PICKED_BEFORE = "before"; |
| /** |
| * This is deprecated in {@link android.os.Build.VERSION_CODES#Q}. |
| * |
| * @deprecated See {@link #ACTION_SUGGESTION_PICKED}. |
| */ |
| @Deprecated |
| public static final String SUGGESTION_SPAN_PICKED_HASHCODE = "hashcode"; |
| |
| public static final int SUGGESTIONS_MAX_SIZE = 5; |
| |
| /* |
| * TODO: Needs to check the validity and add a feature that TextView will change |
| * the current IME to the other IME which is specified in SuggestionSpan. |
| * An IME needs to set the span by specifying the target IME and Subtype of SuggestionSpan. |
| * And the current IME might want to specify any IME as the target IME including other IMEs. |
| */ |
| |
| private int mFlags; |
| private final String[] mSuggestions; |
| /** |
| * Kept for compatibility for apps that rely on invalid locale strings e.g. |
| * {@code new Locale(" an ", " i n v a l i d ", "data")}, which cannot be handled by |
| * {@link #mLanguageTag}. |
| */ |
| @NonNull |
| private final String mLocaleStringForCompatibility; |
| @NonNull |
| private final String mLanguageTag; |
| private final int mHashCode; |
| |
| @UnsupportedAppUsage |
| private float mEasyCorrectUnderlineThickness; |
| @UnsupportedAppUsage |
| private int mEasyCorrectUnderlineColor; |
| |
| private float mMisspelledUnderlineThickness; |
| private int mMisspelledUnderlineColor; |
| |
| private float mAutoCorrectionUnderlineThickness; |
| private int mAutoCorrectionUnderlineColor; |
| |
| /** |
| * @param context Context for the application |
| * @param suggestions Suggestions for the string under the span |
| * @param flags Additional flags indicating how this span is handled in TextView |
| */ |
| public SuggestionSpan(Context context, String[] suggestions, int flags) { |
| this(context, null, suggestions, flags, null); |
| } |
| |
| /** |
| * @param locale Locale of the suggestions |
| * @param suggestions Suggestions for the string under the span |
| * @param flags Additional flags indicating how this span is handled in TextView |
| */ |
| public SuggestionSpan(Locale locale, String[] suggestions, int flags) { |
| this(null, locale, suggestions, flags, null); |
| } |
| |
| /** |
| * @param context Context for the application |
| * @param locale locale Locale of the suggestions |
| * @param suggestions Suggestions for the string under the span. Only the first up to |
| * {@link SuggestionSpan#SUGGESTIONS_MAX_SIZE} will be considered. Null values not permitted. |
| * @param flags Additional flags indicating how this span is handled in TextView |
| * @param notificationTargetClass if not null, this class will get notified when the user |
| * selects one of the suggestions. On Android |
| * {@link android.os.Build.VERSION_CODES#Q} and later this |
| * parameter is always ignored. |
| */ |
| public SuggestionSpan(Context context, Locale locale, String[] suggestions, int flags, |
| Class<?> notificationTargetClass) { |
| final int N = Math.min(SUGGESTIONS_MAX_SIZE, suggestions.length); |
| mSuggestions = Arrays.copyOf(suggestions, N); |
| mFlags = flags; |
| final Locale sourceLocale; |
| if (locale != null) { |
| sourceLocale = locale; |
| } else if (context != null) { |
| // TODO: Consider to context.getResources().getResolvedLocale() instead. |
| sourceLocale = context.getResources().getConfiguration().locale; |
| } else { |
| Log.e("SuggestionSpan", "No locale or context specified in SuggestionSpan constructor"); |
| sourceLocale = null; |
| } |
| mLocaleStringForCompatibility = sourceLocale == null ? "" : sourceLocale.toString(); |
| mLanguageTag = sourceLocale == null ? "" : sourceLocale.toLanguageTag(); |
| mHashCode = hashCodeInternal(mSuggestions, mLanguageTag, mLocaleStringForCompatibility); |
| |
| initStyle(context); |
| } |
| |
| private void initStyle(Context context) { |
| if (context == null) { |
| mMisspelledUnderlineThickness = 0; |
| mEasyCorrectUnderlineThickness = 0; |
| mAutoCorrectionUnderlineThickness = 0; |
| mMisspelledUnderlineColor = Color.BLACK; |
| mEasyCorrectUnderlineColor = Color.BLACK; |
| mAutoCorrectionUnderlineColor = Color.BLACK; |
| return; |
| } |
| |
| int defStyleAttr = com.android.internal.R.attr.textAppearanceMisspelledSuggestion; |
| TypedArray typedArray = context.obtainStyledAttributes( |
| null, com.android.internal.R.styleable.SuggestionSpan, defStyleAttr, 0); |
| mMisspelledUnderlineThickness = typedArray.getDimension( |
| com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0); |
| mMisspelledUnderlineColor = typedArray.getColor( |
| com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK); |
| |
| defStyleAttr = com.android.internal.R.attr.textAppearanceEasyCorrectSuggestion; |
| typedArray = context.obtainStyledAttributes( |
| null, com.android.internal.R.styleable.SuggestionSpan, defStyleAttr, 0); |
| mEasyCorrectUnderlineThickness = typedArray.getDimension( |
| com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0); |
| mEasyCorrectUnderlineColor = typedArray.getColor( |
| com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK); |
| |
| defStyleAttr = com.android.internal.R.attr.textAppearanceAutoCorrectionSuggestion; |
| typedArray = context.obtainStyledAttributes( |
| null, com.android.internal.R.styleable.SuggestionSpan, defStyleAttr, 0); |
| mAutoCorrectionUnderlineThickness = typedArray.getDimension( |
| com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0); |
| mAutoCorrectionUnderlineColor = typedArray.getColor( |
| com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK); |
| } |
| |
| public SuggestionSpan(Parcel src) { |
| mSuggestions = src.readStringArray(); |
| mFlags = src.readInt(); |
| mLocaleStringForCompatibility = src.readString(); |
| mLanguageTag = src.readString(); |
| mHashCode = src.readInt(); |
| mEasyCorrectUnderlineColor = src.readInt(); |
| mEasyCorrectUnderlineThickness = src.readFloat(); |
| mMisspelledUnderlineColor = src.readInt(); |
| mMisspelledUnderlineThickness = src.readFloat(); |
| mAutoCorrectionUnderlineColor = src.readInt(); |
| mAutoCorrectionUnderlineThickness = src.readFloat(); |
| } |
| |
| /** |
| * @return an array of suggestion texts for this span |
| */ |
| public String[] getSuggestions() { |
| return mSuggestions; |
| } |
| |
| /** |
| * @deprecated use {@link #getLocaleObject()} instead. |
| * @return the locale of the suggestions. An empty string is returned if no locale is specified. |
| */ |
| @NonNull |
| @Deprecated |
| public String getLocale() { |
| return mLocaleStringForCompatibility; |
| } |
| |
| /** |
| * Returns a well-formed BCP 47 language tag representation of the suggestions, as a |
| * {@link Locale} object. |
| * |
| * <p><b>Caveat</b>: The returned object is guaranteed to be a a well-formed BCP 47 language tag |
| * representation. For example, this method can return an empty locale rather than returning a |
| * malformed data when this object is initialized with an malformed {@link Locale} object, e.g. |
| * {@code new Locale(" a ", " b c d ", " "}.</p> |
| * |
| * @return the locale of the suggestions. {@code null} is returned if no locale is specified. |
| */ |
| @Nullable |
| public Locale getLocaleObject() { |
| return mLanguageTag.isEmpty() ? null : Locale.forLanguageTag(mLanguageTag); |
| } |
| |
| /** |
| * @return {@code null}. |
| * |
| * @hide |
| * @deprecated Do not use. Always returns {@code null}. |
| */ |
| @Deprecated |
| @UnsupportedAppUsage |
| public String getNotificationTargetClassName() { |
| return null; |
| } |
| |
| public int getFlags() { |
| return mFlags; |
| } |
| |
| public void setFlags(int flags) { |
| mFlags = flags; |
| } |
| |
| @Override |
| public int describeContents() { |
| return 0; |
| } |
| |
| @Override |
| public void writeToParcel(Parcel dest, int flags) { |
| writeToParcelInternal(dest, flags); |
| } |
| |
| /** @hide */ |
| public void writeToParcelInternal(Parcel dest, int flags) { |
| dest.writeStringArray(mSuggestions); |
| dest.writeInt(mFlags); |
| dest.writeString(mLocaleStringForCompatibility); |
| dest.writeString(mLanguageTag); |
| dest.writeInt(mHashCode); |
| dest.writeInt(mEasyCorrectUnderlineColor); |
| dest.writeFloat(mEasyCorrectUnderlineThickness); |
| dest.writeInt(mMisspelledUnderlineColor); |
| dest.writeFloat(mMisspelledUnderlineThickness); |
| dest.writeInt(mAutoCorrectionUnderlineColor); |
| dest.writeFloat(mAutoCorrectionUnderlineThickness); |
| } |
| |
| @Override |
| public int getSpanTypeId() { |
| return getSpanTypeIdInternal(); |
| } |
| |
| /** @hide */ |
| public int getSpanTypeIdInternal() { |
| return TextUtils.SUGGESTION_SPAN; |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (o instanceof SuggestionSpan) { |
| return ((SuggestionSpan)o).hashCode() == mHashCode; |
| } |
| return false; |
| } |
| |
| @Override |
| public int hashCode() { |
| return mHashCode; |
| } |
| |
| private static int hashCodeInternal(String[] suggestions, @NonNull String languageTag, |
| @NonNull String localeStringForCompatibility) { |
| return Arrays.hashCode(new Object[] {Long.valueOf(SystemClock.uptimeMillis()), suggestions, |
| languageTag, localeStringForCompatibility}); |
| } |
| |
| public static final @android.annotation.NonNull Parcelable.Creator<SuggestionSpan> CREATOR = |
| new Parcelable.Creator<SuggestionSpan>() { |
| @Override |
| public SuggestionSpan createFromParcel(Parcel source) { |
| return new SuggestionSpan(source); |
| } |
| |
| @Override |
| public SuggestionSpan[] newArray(int size) { |
| return new SuggestionSpan[size]; |
| } |
| }; |
| |
| @Override |
| public void updateDrawState(TextPaint tp) { |
| final boolean misspelled = (mFlags & FLAG_MISSPELLED) != 0; |
| final boolean easy = (mFlags & FLAG_EASY_CORRECT) != 0; |
| final boolean autoCorrection = (mFlags & FLAG_AUTO_CORRECTION) != 0; |
| if (easy) { |
| if (!misspelled) { |
| tp.setUnderlineText(mEasyCorrectUnderlineColor, mEasyCorrectUnderlineThickness); |
| } else if (tp.underlineColor == 0) { |
| // Spans are rendered in an arbitrary order. Since misspelled is less prioritary |
| // than just easy, do not apply misspelled if an easy (or a mispelled) has been set |
| tp.setUnderlineText(mMisspelledUnderlineColor, mMisspelledUnderlineThickness); |
| } |
| } else if (autoCorrection) { |
| tp.setUnderlineText(mAutoCorrectionUnderlineColor, mAutoCorrectionUnderlineThickness); |
| } |
| } |
| |
| /** |
| * @return The color of the underline for that span, or 0 if there is no underline |
| */ |
| @ColorInt |
| public int getUnderlineColor() { |
| // The order here should match what is used in updateDrawState |
| final boolean misspelled = (mFlags & FLAG_MISSPELLED) != 0; |
| final boolean easy = (mFlags & FLAG_EASY_CORRECT) != 0; |
| final boolean autoCorrection = (mFlags & FLAG_AUTO_CORRECTION) != 0; |
| if (easy) { |
| if (!misspelled) { |
| return mEasyCorrectUnderlineColor; |
| } else { |
| return mMisspelledUnderlineColor; |
| } |
| } else if (autoCorrection) { |
| return mAutoCorrectionUnderlineColor; |
| } |
| return 0; |
| } |
| |
| /** |
| * Does nothing. |
| * |
| * @deprecated this is deprecated in {@link android.os.Build.VERSION_CODES#Q}. |
| * @hide |
| */ |
| @UnsupportedAppUsage |
| @Deprecated |
| public void notifySelection(Context context, String original, int index) { |
| Log.w(TAG, "notifySelection() is deprecated. Does nothing."); |
| } |
| } |