/* * 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.NonNull; import android.annotation.Nullable; import android.content.Context; import android.content.Intent; 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.view.inputmethod.InputMethodManager; 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; public static final String ACTION_SUGGESTION_PICKED = "android.text.style.SUGGESTION_PICKED"; public static final String SUGGESTION_SPAN_PICKED_AFTER = "after"; public static final String SUGGESTION_SPAN_PICKED_BEFORE = "before"; 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 String mNotificationTargetClassName; private final String mNotificationTargetPackageName; private final int mHashCode; private float mEasyCorrectUnderlineThickness; 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. */ 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(); if (context != null) { mNotificationTargetPackageName = context.getPackageName(); } else { mNotificationTargetPackageName = null; } if (notificationTargetClass != null) { mNotificationTargetClassName = notificationTargetClass.getCanonicalName(); } else { mNotificationTargetClassName = ""; } mHashCode = hashCodeInternal(mSuggestions, mLanguageTag, mLocaleStringForCompatibility, mNotificationTargetClassName); 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(); mNotificationTargetClassName = src.readString(); mNotificationTargetPackageName = 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. * *

Caveat: 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 ", " "}.

* * @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 The name of the class to notify. The class of the original IME package will receive * a notification when the user selects one of the suggestions. The notification will include * the original string, the suggested replacement string as well as the hashCode of this span. * The class will get notified by an intent that has those information. * This is an internal API because only the framework should know the class name. * * @hide */ public String getNotificationTargetClassName() { return mNotificationTargetClassName; } 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.writeString(mNotificationTargetClassName); dest.writeString(mNotificationTargetPackageName); 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, String notificationTargetClassName) { return Arrays.hashCode(new Object[] {Long.valueOf(SystemClock.uptimeMillis()), suggestions, languageTag, localeStringForCompatibility, notificationTargetClassName}); } public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { @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 * * @hide */ 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; } /** * Notifies a suggestion selection. * * @hide */ public void notifySelection(Context context, String original, int index) { final Intent intent = new Intent(); if (context == null || mNotificationTargetClassName == null) { return; } // Ensures that only a class in the original IME package will receive the // notification. if (mSuggestions == null || index < 0 || index >= mSuggestions.length) { Log.w(TAG, "Unable to notify the suggestion as the index is out of range index=" + index + " length=" + mSuggestions.length); return; } // The package name is not mandatory (legacy from JB), and if the package name // is missing, we try to notify the suggestion through the input method manager. if (mNotificationTargetPackageName != null) { intent.setClassName(mNotificationTargetPackageName, mNotificationTargetClassName); intent.setAction(SuggestionSpan.ACTION_SUGGESTION_PICKED); intent.putExtra(SuggestionSpan.SUGGESTION_SPAN_PICKED_BEFORE, original); intent.putExtra(SuggestionSpan.SUGGESTION_SPAN_PICKED_AFTER, mSuggestions[index]); intent.putExtra(SuggestionSpan.SUGGESTION_SPAN_PICKED_HASHCODE, hashCode()); context.sendBroadcast(intent); } else { InputMethodManager imm = InputMethodManager.peekInstance(); if (imm != null) { imm.notifySuggestionPicked(this, original, index); } } } }