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         writeToParcelInternal(dest, flags);
    252     }
    253 
    254     /** @hide */
    255     public void writeToParcelInternal(Parcel dest, int flags) {
    256         dest.writeStringArray(mSuggestions);
    257         dest.writeInt(mFlags);
    258         dest.writeString(mLocaleString);
    259         dest.writeString(mNotificationTargetClassName);
    260         dest.writeString(mNotificationTargetPackageName);
    261         dest.writeInt(mHashCode);
    262         dest.writeInt(mEasyCorrectUnderlineColor);
    263         dest.writeFloat(mEasyCorrectUnderlineThickness);
    264         dest.writeInt(mMisspelledUnderlineColor);
    265         dest.writeFloat(mMisspelledUnderlineThickness);
    266         dest.writeInt(mAutoCorrectionUnderlineColor);
    267         dest.writeFloat(mAutoCorrectionUnderlineThickness);
    268     }
    269 
    270     @Override
    271     public int getSpanTypeId() {
    272         return getSpanTypeIdInternal();
    273     }
    274 
    275     /** @hide */
    276     public int getSpanTypeIdInternal() {
    277         return TextUtils.SUGGESTION_SPAN;
    278     }
    279 
    280     @Override
    281     public boolean equals(Object o) {
    282         if (o instanceof SuggestionSpan) {
    283             return ((SuggestionSpan)o).hashCode() == mHashCode;
    284         }
    285         return false;
    286     }
    287 
    288     @Override
    289     public int hashCode() {
    290         return mHashCode;
    291     }
    292 
    293     private static int hashCodeInternal(String[] suggestions, String locale,
    294             String notificationTargetClassName) {
    295         return Arrays.hashCode(new Object[] {Long.valueOf(SystemClock.uptimeMillis()), suggestions,
    296                 locale, notificationTargetClassName});
    297     }
    298 
    299     public static final Parcelable.Creator<SuggestionSpan> CREATOR =
    300             new Parcelable.Creator<SuggestionSpan>() {
    301         @Override
    302         public SuggestionSpan createFromParcel(Parcel source) {
    303             return new SuggestionSpan(source);
    304         }
    305 
    306         @Override
    307         public SuggestionSpan[] newArray(int size) {
    308             return new SuggestionSpan[size];
    309         }
    310     };
    311 
    312     @Override
    313     public void updateDrawState(TextPaint tp) {
    314         final boolean misspelled = (mFlags & FLAG_MISSPELLED) != 0;
    315         final boolean easy = (mFlags & FLAG_EASY_CORRECT) != 0;
    316         final boolean autoCorrection = (mFlags & FLAG_AUTO_CORRECTION) != 0;
    317         if (easy) {
    318             if (!misspelled) {
    319                 tp.setUnderlineText(mEasyCorrectUnderlineColor, mEasyCorrectUnderlineThickness);
    320             } else if (tp.underlineColor == 0) {
    321                 // Spans are rendered in an arbitrary order. Since misspelled is less prioritary
    322                 // than just easy, do not apply misspelled if an easy (or a mispelled) has been set
    323                 tp.setUnderlineText(mMisspelledUnderlineColor, mMisspelledUnderlineThickness);
    324             }
    325         } else if (autoCorrection) {
    326             tp.setUnderlineText(mAutoCorrectionUnderlineColor, mAutoCorrectionUnderlineThickness);
    327         }
    328     }
    329 
    330     /**
    331      * @return The color of the underline for that span, or 0 if there is no underline
    332      *
    333      * @hide
    334      */
    335     public int getUnderlineColor() {
    336         // The order here should match what is used in updateDrawState
    337         final boolean misspelled = (mFlags & FLAG_MISSPELLED) != 0;
    338         final boolean easy = (mFlags & FLAG_EASY_CORRECT) != 0;
    339         final boolean autoCorrection = (mFlags & FLAG_AUTO_CORRECTION) != 0;
    340         if (easy) {
    341             if (!misspelled) {
    342                 return mEasyCorrectUnderlineColor;
    343             } else {
    344                 return mMisspelledUnderlineColor;
    345             }
    346         } else if (autoCorrection) {
    347             return mAutoCorrectionUnderlineColor;
    348         }
    349         return 0;
    350     }
    351 
    352     /**
    353      * Notifies a suggestion selection.
    354      *
    355      * @hide
    356      */
    357     public void notifySelection(Context context, String original, int index) {
    358         final Intent intent = new Intent();
    359 
    360         if (context == null || mNotificationTargetClassName == null) {
    361             return;
    362         }
    363         // Ensures that only a class in the original IME package will receive the
    364         // notification.
    365         if (mSuggestions == null || index < 0 || index >= mSuggestions.length) {
    366             Log.w(TAG, "Unable to notify the suggestion as the index is out of range index=" + index
    367                     + " length=" + mSuggestions.length);
    368             return;
    369         }
    370 
    371         // The package name is not mandatory (legacy from JB), and if the package name
    372         // is missing, we try to notify the suggestion through the input method manager.
    373         if (mNotificationTargetPackageName != null) {
    374             intent.setClassName(mNotificationTargetPackageName, mNotificationTargetClassName);
    375             intent.setAction(SuggestionSpan.ACTION_SUGGESTION_PICKED);
    376             intent.putExtra(SuggestionSpan.SUGGESTION_SPAN_PICKED_BEFORE, original);
    377             intent.putExtra(SuggestionSpan.SUGGESTION_SPAN_PICKED_AFTER, mSuggestions[index]);
    378             intent.putExtra(SuggestionSpan.SUGGESTION_SPAN_PICKED_HASHCODE, hashCode());
    379             context.sendBroadcast(intent);
    380         } else {
    381             InputMethodManager imm = InputMethodManager.peekInstance();
    382             if (imm != null) {
    383                 imm.notifySuggestionPicked(this, original, index);
    384             }
    385         }
    386     }
    387 }
    388