Home | History | Annotate | Download | only in style
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.text.style;
     18 
     19 import android.annotation.NonNull;
     20 import android.annotation.Nullable;
     21 import android.content.Context;
     22 import android.content.Intent;
     23 import android.content.res.TypedArray;
     24 import android.graphics.Color;
     25 import android.os.Parcel;
     26 import android.os.Parcelable;
     27 import android.os.SystemClock;
     28 import android.text.ParcelableSpan;
     29 import android.text.TextPaint;
     30 import android.text.TextUtils;
     31 import android.util.Log;
     32 import android.view.inputmethod.InputMethodManager;
     33 import android.widget.TextView;
     34 
     35 import java.util.Arrays;
     36 import java.util.Locale;
     37 
     38 /**
     39  * Holds suggestion candidates for the text enclosed in this span.
     40  *
     41  * When such a span is edited in an EditText, double tapping on the text enclosed in this span will
     42  * display a popup dialog listing suggestion replacement for that text. The user can then replace
     43  * the original text by one of the suggestions.
     44  *
     45  * These spans should typically be created by the input method to provide correction and alternates
     46  * for the text.
     47  *
     48  * @see TextView#isSuggestionsEnabled()
     49  */
     50 public class SuggestionSpan extends CharacterStyle implements ParcelableSpan {
     51 
     52     private static final String TAG = "SuggestionSpan";
     53 
     54     /**
     55      * Sets this flag if the suggestions should be easily accessible with few interactions.
     56      * This flag should be set for every suggestions that the user is likely to use.
     57      */
     58     public static final int FLAG_EASY_CORRECT = 0x0001;
     59 
     60     /**
     61      * Sets this flag if the suggestions apply to a misspelled word/text. This type of suggestion is
     62      * rendered differently to highlight the error.
     63      */
     64     public static final int FLAG_MISSPELLED = 0x0002;
     65 
     66     /**
     67      * Sets this flag if the auto correction is about to be applied to a word/text
     68      * that the user is typing/composing. This type of suggestion is rendered differently
     69      * to indicate the auto correction is happening.
     70      */
     71     public static final int FLAG_AUTO_CORRECTION = 0x0004;
     72 
     73     public static final String ACTION_SUGGESTION_PICKED = "android.text.style.SUGGESTION_PICKED";
     74     public static final String SUGGESTION_SPAN_PICKED_AFTER = "after";
     75     public static final String SUGGESTION_SPAN_PICKED_BEFORE = "before";
     76     public static final String SUGGESTION_SPAN_PICKED_HASHCODE = "hashcode";
     77 
     78     public static final int SUGGESTIONS_MAX_SIZE = 5;
     79 
     80     /*
     81      * TODO: Needs to check the validity and add a feature that TextView will change
     82      * the current IME to the other IME which is specified in SuggestionSpan.
     83      * An IME needs to set the span by specifying the target IME and Subtype of SuggestionSpan.
     84      * And the current IME might want to specify any IME as the target IME including other IMEs.
     85      */
     86 
     87     private int mFlags;
     88     private final String[] mSuggestions;
     89     /**
     90      * Kept for compatibility for apps that rely on invalid locale strings e.g.
     91      * {@code new Locale(" an ", " i n v a l i d ", "data")}, which cannot be handled by
     92      * {@link #mLanguageTag}.
     93      */
     94     @NonNull
     95     private final String mLocaleStringForCompatibility;
     96     @NonNull
     97     private final String mLanguageTag;
     98     private final String mNotificationTargetClassName;
     99     private final String mNotificationTargetPackageName;
    100     private final int mHashCode;
    101 
    102     private float mEasyCorrectUnderlineThickness;
    103     private int mEasyCorrectUnderlineColor;
    104 
    105     private float mMisspelledUnderlineThickness;
    106     private int mMisspelledUnderlineColor;
    107 
    108     private float mAutoCorrectionUnderlineThickness;
    109     private int mAutoCorrectionUnderlineColor;
    110 
    111     /**
    112      * @param context Context for the application
    113      * @param suggestions Suggestions for the string under the span
    114      * @param flags Additional flags indicating how this span is handled in TextView
    115      */
    116     public SuggestionSpan(Context context, String[] suggestions, int flags) {
    117         this(context, null, suggestions, flags, null);
    118     }
    119 
    120     /**
    121      * @param locale Locale of the suggestions
    122      * @param suggestions Suggestions for the string under the span
    123      * @param flags Additional flags indicating how this span is handled in TextView
    124      */
    125     public SuggestionSpan(Locale locale, String[] suggestions, int flags) {
    126         this(null, locale, suggestions, flags, null);
    127     }
    128 
    129     /**
    130      * @param context Context for the application
    131      * @param locale locale Locale of the suggestions
    132      * @param suggestions Suggestions for the string under the span. Only the first up to
    133      * {@link SuggestionSpan#SUGGESTIONS_MAX_SIZE} will be considered. Null values not permitted.
    134      * @param flags Additional flags indicating how this span is handled in TextView
    135      * @param notificationTargetClass if not null, this class will get notified when the user
    136      * selects one of the suggestions.
    137      */
    138     public SuggestionSpan(Context context, Locale locale, String[] suggestions, int flags,
    139             Class<?> notificationTargetClass) {
    140         final int N = Math.min(SUGGESTIONS_MAX_SIZE, suggestions.length);
    141         mSuggestions = Arrays.copyOf(suggestions, N);
    142         mFlags = flags;
    143         final Locale sourceLocale;
    144         if (locale != null) {
    145             sourceLocale = locale;
    146         } else if (context != null) {
    147             // TODO: Consider to context.getResources().getResolvedLocale() instead.
    148             sourceLocale = context.getResources().getConfiguration().locale;
    149         } else {
    150             Log.e("SuggestionSpan", "No locale or context specified in SuggestionSpan constructor");
    151             sourceLocale = null;
    152         }
    153         mLocaleStringForCompatibility = sourceLocale == null ? "" : sourceLocale.toString();
    154         mLanguageTag = sourceLocale == null ? "" : sourceLocale.toLanguageTag();
    155 
    156         if (context != null) {
    157             mNotificationTargetPackageName = context.getPackageName();
    158         } else {
    159             mNotificationTargetPackageName = null;
    160         }
    161 
    162         if (notificationTargetClass != null) {
    163             mNotificationTargetClassName = notificationTargetClass.getCanonicalName();
    164         } else {
    165             mNotificationTargetClassName = "";
    166         }
    167         mHashCode = hashCodeInternal(mSuggestions, mLanguageTag, mLocaleStringForCompatibility,
    168                 mNotificationTargetClassName);
    169 
    170         initStyle(context);
    171     }
    172 
    173     private void initStyle(Context context) {
    174         if (context == null) {
    175             mMisspelledUnderlineThickness = 0;
    176             mEasyCorrectUnderlineThickness = 0;
    177             mAutoCorrectionUnderlineThickness = 0;
    178             mMisspelledUnderlineColor = Color.BLACK;
    179             mEasyCorrectUnderlineColor = Color.BLACK;
    180             mAutoCorrectionUnderlineColor = Color.BLACK;
    181             return;
    182         }
    183 
    184         int defStyleAttr = com.android.internal.R.attr.textAppearanceMisspelledSuggestion;
    185         TypedArray typedArray = context.obtainStyledAttributes(
    186                 null, com.android.internal.R.styleable.SuggestionSpan, defStyleAttr, 0);
    187         mMisspelledUnderlineThickness = typedArray.getDimension(
    188                 com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0);
    189         mMisspelledUnderlineColor = typedArray.getColor(
    190                 com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK);
    191 
    192         defStyleAttr = com.android.internal.R.attr.textAppearanceEasyCorrectSuggestion;
    193         typedArray = context.obtainStyledAttributes(
    194                 null, com.android.internal.R.styleable.SuggestionSpan, defStyleAttr, 0);
    195         mEasyCorrectUnderlineThickness = typedArray.getDimension(
    196                 com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0);
    197         mEasyCorrectUnderlineColor = typedArray.getColor(
    198                 com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK);
    199 
    200         defStyleAttr = com.android.internal.R.attr.textAppearanceAutoCorrectionSuggestion;
    201         typedArray = context.obtainStyledAttributes(
    202                 null, com.android.internal.R.styleable.SuggestionSpan, defStyleAttr, 0);
    203         mAutoCorrectionUnderlineThickness = typedArray.getDimension(
    204                 com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0);
    205         mAutoCorrectionUnderlineColor = typedArray.getColor(
    206                 com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK);
    207     }
    208 
    209     public SuggestionSpan(Parcel src) {
    210         mSuggestions = src.readStringArray();
    211         mFlags = src.readInt();
    212         mLocaleStringForCompatibility = src.readString();
    213         mLanguageTag = src.readString();
    214         mNotificationTargetClassName = src.readString();
    215         mNotificationTargetPackageName = src.readString();
    216         mHashCode = src.readInt();
    217         mEasyCorrectUnderlineColor = src.readInt();
    218         mEasyCorrectUnderlineThickness = src.readFloat();
    219         mMisspelledUnderlineColor = src.readInt();
    220         mMisspelledUnderlineThickness = src.readFloat();
    221         mAutoCorrectionUnderlineColor = src.readInt();
    222         mAutoCorrectionUnderlineThickness = src.readFloat();
    223     }
    224 
    225     /**
    226      * @return an array of suggestion texts for this span
    227      */
    228     public String[] getSuggestions() {
    229         return mSuggestions;
    230     }
    231 
    232     /**
    233      * @deprecated use {@link #getLocaleObject()} instead.
    234      * @return the locale of the suggestions. An empty string is returned if no locale is specified.
    235      */
    236     @NonNull
    237     @Deprecated
    238     public String getLocale() {
    239         return mLocaleStringForCompatibility;
    240     }
    241 
    242     /**
    243      * Returns a well-formed BCP 47 language tag representation of the suggestions, as a
    244      * {@link Locale} object.
    245      *
    246      * <p><b>Caveat</b>: The returned object is guaranteed to be a  a well-formed BCP 47 language tag
    247      * representation.  For example, this method can return an empty locale rather than returning a
    248      * malformed data when this object is initialized with an malformed {@link Locale} object, e.g.
    249      * {@code new Locale(" a ", " b c d ", " "}.</p>
    250      *
    251      * @return the locale of the suggestions. {@code null} is returned if no locale is specified.
    252      */
    253     @Nullable
    254     public Locale getLocaleObject() {
    255         return mLanguageTag.isEmpty() ? null : Locale.forLanguageTag(mLanguageTag);
    256     }
    257 
    258     /**
    259      * @return The name of the class to notify. The class of the original IME package will receive
    260      * a notification when the user selects one of the suggestions. The notification will include
    261      * the original string, the suggested replacement string as well as the hashCode of this span.
    262      * The class will get notified by an intent that has those information.
    263      * This is an internal API because only the framework should know the class name.
    264      *
    265      * @hide
    266      */
    267     public String getNotificationTargetClassName() {
    268         return mNotificationTargetClassName;
    269     }
    270 
    271     public int getFlags() {
    272         return mFlags;
    273     }
    274 
    275     public void setFlags(int flags) {
    276         mFlags = flags;
    277     }
    278 
    279     @Override
    280     public int describeContents() {
    281         return 0;
    282     }
    283 
    284     @Override
    285     public void writeToParcel(Parcel dest, int flags) {
    286         writeToParcelInternal(dest, flags);
    287     }
    288 
    289     /** @hide */
    290     public void writeToParcelInternal(Parcel dest, int flags) {
    291         dest.writeStringArray(mSuggestions);
    292         dest.writeInt(mFlags);
    293         dest.writeString(mLocaleStringForCompatibility);
    294         dest.writeString(mLanguageTag);
    295         dest.writeString(mNotificationTargetClassName);
    296         dest.writeString(mNotificationTargetPackageName);
    297         dest.writeInt(mHashCode);
    298         dest.writeInt(mEasyCorrectUnderlineColor);
    299         dest.writeFloat(mEasyCorrectUnderlineThickness);
    300         dest.writeInt(mMisspelledUnderlineColor);
    301         dest.writeFloat(mMisspelledUnderlineThickness);
    302         dest.writeInt(mAutoCorrectionUnderlineColor);
    303         dest.writeFloat(mAutoCorrectionUnderlineThickness);
    304     }
    305 
    306     @Override
    307     public int getSpanTypeId() {
    308         return getSpanTypeIdInternal();
    309     }
    310 
    311     /** @hide */
    312     public int getSpanTypeIdInternal() {
    313         return TextUtils.SUGGESTION_SPAN;
    314     }
    315 
    316     @Override
    317     public boolean equals(Object o) {
    318         if (o instanceof SuggestionSpan) {
    319             return ((SuggestionSpan)o).hashCode() == mHashCode;
    320         }
    321         return false;
    322     }
    323 
    324     @Override
    325     public int hashCode() {
    326         return mHashCode;
    327     }
    328 
    329     private static int hashCodeInternal(String[] suggestions, @NonNull String languageTag,
    330             @NonNull String localeStringForCompatibility, String notificationTargetClassName) {
    331         return Arrays.hashCode(new Object[] {Long.valueOf(SystemClock.uptimeMillis()), suggestions,
    332                 languageTag, localeStringForCompatibility, notificationTargetClassName});
    333     }
    334 
    335     public static final Parcelable.Creator<SuggestionSpan> CREATOR =
    336             new Parcelable.Creator<SuggestionSpan>() {
    337         @Override
    338         public SuggestionSpan createFromParcel(Parcel source) {
    339             return new SuggestionSpan(source);
    340         }
    341 
    342         @Override
    343         public SuggestionSpan[] newArray(int size) {
    344             return new SuggestionSpan[size];
    345         }
    346     };
    347 
    348     @Override
    349     public void updateDrawState(TextPaint tp) {
    350         final boolean misspelled = (mFlags & FLAG_MISSPELLED) != 0;
    351         final boolean easy = (mFlags & FLAG_EASY_CORRECT) != 0;
    352         final boolean autoCorrection = (mFlags & FLAG_AUTO_CORRECTION) != 0;
    353         if (easy) {
    354             if (!misspelled) {
    355                 tp.setUnderlineText(mEasyCorrectUnderlineColor, mEasyCorrectUnderlineThickness);
    356             } else if (tp.underlineColor == 0) {
    357                 // Spans are rendered in an arbitrary order. Since misspelled is less prioritary
    358                 // than just easy, do not apply misspelled if an easy (or a mispelled) has been set
    359                 tp.setUnderlineText(mMisspelledUnderlineColor, mMisspelledUnderlineThickness);
    360             }
    361         } else if (autoCorrection) {
    362             tp.setUnderlineText(mAutoCorrectionUnderlineColor, mAutoCorrectionUnderlineThickness);
    363         }
    364     }
    365 
    366     /**
    367      * @return The color of the underline for that span, or 0 if there is no underline
    368      *
    369      * @hide
    370      */
    371     public int getUnderlineColor() {
    372         // The order here should match what is used in updateDrawState
    373         final boolean misspelled = (mFlags & FLAG_MISSPELLED) != 0;
    374         final boolean easy = (mFlags & FLAG_EASY_CORRECT) != 0;
    375         final boolean autoCorrection = (mFlags & FLAG_AUTO_CORRECTION) != 0;
    376         if (easy) {
    377             if (!misspelled) {
    378                 return mEasyCorrectUnderlineColor;
    379             } else {
    380                 return mMisspelledUnderlineColor;
    381             }
    382         } else if (autoCorrection) {
    383             return mAutoCorrectionUnderlineColor;
    384         }
    385         return 0;
    386     }
    387 
    388     /**
    389      * Notifies a suggestion selection.
    390      *
    391      * @hide
    392      */
    393     public void notifySelection(Context context, String original, int index) {
    394         final Intent intent = new Intent();
    395 
    396         if (context == null || mNotificationTargetClassName == null) {
    397             return;
    398         }
    399         // Ensures that only a class in the original IME package will receive the
    400         // notification.
    401         if (mSuggestions == null || index < 0 || index >= mSuggestions.length) {
    402             Log.w(TAG, "Unable to notify the suggestion as the index is out of range index=" + index
    403                     + " length=" + mSuggestions.length);
    404             return;
    405         }
    406 
    407         // The package name is not mandatory (legacy from JB), and if the package name
    408         // is missing, we try to notify the suggestion through the input method manager.
    409         if (mNotificationTargetPackageName != null) {
    410             intent.setClassName(mNotificationTargetPackageName, mNotificationTargetClassName);
    411             intent.setAction(SuggestionSpan.ACTION_SUGGESTION_PICKED);
    412             intent.putExtra(SuggestionSpan.SUGGESTION_SPAN_PICKED_BEFORE, original);
    413             intent.putExtra(SuggestionSpan.SUGGESTION_SPAN_PICKED_AFTER, mSuggestions[index]);
    414             intent.putExtra(SuggestionSpan.SUGGESTION_SPAN_PICKED_HASHCODE, hashCode());
    415             context.sendBroadcast(intent);
    416         } else {
    417             InputMethodManager imm = InputMethodManager.peekInstance();
    418             if (imm != null) {
    419                 imm.notifySuggestionPicked(this, original, index);
    420             }
    421         }
    422     }
    423 }
    424