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