SuggestionSpan.java revision 51322732739b355dd842abc9cef34df8613b2626
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
17package android.text.style;
18
19import android.content.Context;
20import android.content.res.TypedArray;
21import android.graphics.Color;
22import android.os.Parcel;
23import android.os.Parcelable;
24import android.os.SystemClock;
25import android.text.ParcelableSpan;
26import android.text.TextPaint;
27import android.text.TextUtils;
28import android.widget.TextView;
29
30import java.util.Arrays;
31import java.util.Locale;
32
33/**
34 * Holds suggestion candidates for the text enclosed in this span.
35 *
36 * When such a span is edited in an EditText, double tapping on the text enclosed in this span will
37 * display a popup dialog listing suggestion replacement for that text. The user can then replace
38 * the original text by one of the suggestions.
39 *
40 * These spans should typically be created by the input method to provide correction and alternates
41 * for the text.
42 *
43 * @see TextView#isSuggestionsEnabled()
44 */
45public class SuggestionSpan extends CharacterStyle implements ParcelableSpan {
46
47    /**
48     * Sets this flag if the suggestions should be easily accessible with few interactions.
49     * This flag should be set for every suggestions that the user is likely to use.
50     */
51    public static final int FLAG_EASY_CORRECT = 0x0001;
52
53    /**
54     * Sets this flag if the suggestions apply to a misspelled word/text. This type of suggestion is
55     * rendered differently to highlight the error.
56     */
57    public static final int FLAG_MISSPELLED = 0x0002;
58
59    public static final String ACTION_SUGGESTION_PICKED = "android.text.style.SUGGESTION_PICKED";
60    public static final String SUGGESTION_SPAN_PICKED_AFTER = "after";
61    public static final String SUGGESTION_SPAN_PICKED_BEFORE = "before";
62    public static final String SUGGESTION_SPAN_PICKED_HASHCODE = "hashcode";
63
64    public static final int SUGGESTIONS_MAX_SIZE = 5;
65
66    /*
67     * TODO: Needs to check the validity and add a feature that TextView will change
68     * the current IME to the other IME which is specified in SuggestionSpan.
69     * An IME needs to set the span by specifying the target IME and Subtype of SuggestionSpan.
70     * And the current IME might want to specify any IME as the target IME including other IMEs.
71     */
72
73    private int mFlags;
74    private final String[] mSuggestions;
75    private final String mLocaleString;
76    private final String mNotificationTargetClassName;
77    private final int mHashCode;
78
79    private float mEasyCorrectUnderlineThickness;
80    private int mEasyCorrectUnderlineColor;
81
82    private float mMisspelledUnderlineThickness;
83    private int mMisspelledUnderlineColor;
84
85    /*
86     * TODO: If switching IME is required, needs to add parameters for ids of InputMethodInfo
87     * and InputMethodSubtype.
88     */
89
90    /**
91     * @param context Context for the application
92     * @param suggestions Suggestions for the string under the span
93     * @param flags Additional flags indicating how this span is handled in TextView
94     */
95    public SuggestionSpan(Context context, String[] suggestions, int flags) {
96        this(context, null, suggestions, flags, null);
97    }
98
99    /**
100     * @param locale Locale of the suggestions
101     * @param suggestions Suggestions for the string under the span
102     * @param flags Additional flags indicating how this span is handled in TextView
103     */
104    public SuggestionSpan(Locale locale, String[] suggestions, int flags) {
105        this(null, locale, suggestions, flags, null);
106    }
107
108    /**
109     * @param context Context for the application
110     * @param locale locale Locale of the suggestions
111     * @param suggestions Suggestions for the string under the span
112     * @param flags Additional flags indicating how this span is handled in TextView
113     * @param notificationTargetClass if not null, this class will get notified when the user
114     * selects one of the suggestions.
115     */
116    public SuggestionSpan(Context context, Locale locale, String[] suggestions, int flags,
117            Class<?> notificationTargetClass) {
118        final int N = Math.min(SUGGESTIONS_MAX_SIZE, suggestions.length);
119        mSuggestions = Arrays.copyOf(suggestions, N);
120        mFlags = flags;
121        if (context != null && locale == null) {
122            mLocaleString = context.getResources().getConfiguration().locale.toString();
123        } else {
124            mLocaleString = locale.toString();
125        }
126
127        if (notificationTargetClass != null) {
128            mNotificationTargetClassName = notificationTargetClass.getCanonicalName();
129        } else {
130            mNotificationTargetClassName = "";
131        }
132        mHashCode = hashCodeInternal(mSuggestions, mLocaleString, mNotificationTargetClassName);
133
134        initStyle(context);
135    }
136
137    private void initStyle(Context context) {
138        int defStyle = com.android.internal.R.attr.textAppearanceMisspelledSuggestion;
139        TypedArray typedArray = context.obtainStyledAttributes(
140                null, com.android.internal.R.styleable.SuggestionSpan, defStyle, 0);
141        mMisspelledUnderlineThickness = typedArray.getDimension(
142                com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0);
143        mMisspelledUnderlineColor = typedArray.getColor(
144                com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK);
145
146        defStyle = com.android.internal.R.attr.textAppearanceEasyCorrectSuggestion;
147
148        typedArray = context.obtainStyledAttributes(
149                null, com.android.internal.R.styleable.SuggestionSpan, defStyle, 0);
150
151        mEasyCorrectUnderlineThickness = typedArray.getDimension(
152                com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0);
153        mEasyCorrectUnderlineColor = typedArray.getColor(
154                com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK);
155    }
156
157    public SuggestionSpan(Parcel src) {
158        mSuggestions = src.readStringArray();
159        mFlags = src.readInt();
160        mLocaleString = src.readString();
161        mNotificationTargetClassName = src.readString();
162        mHashCode = src.readInt();
163        mEasyCorrectUnderlineColor = src.readInt();
164        mEasyCorrectUnderlineThickness = src.readFloat();
165        mMisspelledUnderlineColor = src.readInt();
166        mMisspelledUnderlineThickness = src.readFloat();
167    }
168
169    /**
170     * @return an array of suggestion texts for this span
171     */
172    public String[] getSuggestions() {
173        return mSuggestions;
174    }
175
176    /**
177     * @return the locale of the suggestions
178     */
179    public String getLocale() {
180        return mLocaleString;
181    }
182
183    /**
184     * @return The name of the class to notify. The class of the original IME package will receive
185     * a notification when the user selects one of the suggestions. The notification will include
186     * the original string, the suggested replacement string as well as the hashCode of this span.
187     * The class will get notified by an intent that has those information.
188     * This is an internal API because only the framework should know the class name.
189     *
190     * @hide
191     */
192    public String getNotificationTargetClassName() {
193        return mNotificationTargetClassName;
194    }
195
196    public int getFlags() {
197        return mFlags;
198    }
199
200    public void setFlags(int flags) {
201        mFlags = flags;
202    }
203
204    @Override
205    public int describeContents() {
206        return 0;
207    }
208
209    @Override
210    public void writeToParcel(Parcel dest, int flags) {
211        dest.writeStringArray(mSuggestions);
212        dest.writeInt(mFlags);
213        dest.writeString(mLocaleString);
214        dest.writeString(mNotificationTargetClassName);
215        dest.writeInt(mHashCode);
216        dest.writeInt(mEasyCorrectUnderlineColor);
217        dest.writeFloat(mEasyCorrectUnderlineThickness);
218        dest.writeInt(mMisspelledUnderlineColor);
219        dest.writeFloat(mMisspelledUnderlineThickness);
220    }
221
222    @Override
223    public int getSpanTypeId() {
224        return TextUtils.SUGGESTION_SPAN;
225    }
226
227    @Override
228    public boolean equals(Object o) {
229        if (o instanceof SuggestionSpan) {
230            return ((SuggestionSpan)o).hashCode() == mHashCode;
231        }
232        return false;
233    }
234
235    @Override
236    public int hashCode() {
237        return mHashCode;
238    }
239
240    private static int hashCodeInternal(String[] suggestions, String locale,
241            String notificationTargetClassName) {
242        return Arrays.hashCode(new Object[] {Long.valueOf(SystemClock.uptimeMillis()), suggestions,
243                locale, notificationTargetClassName});
244    }
245
246    public static final Parcelable.Creator<SuggestionSpan> CREATOR =
247            new Parcelable.Creator<SuggestionSpan>() {
248        @Override
249        public SuggestionSpan createFromParcel(Parcel source) {
250            return new SuggestionSpan(source);
251        }
252
253        @Override
254        public SuggestionSpan[] newArray(int size) {
255            return new SuggestionSpan[size];
256        }
257    };
258
259    @Override
260    public void updateDrawState(TextPaint tp) {
261        if ((mFlags & FLAG_MISSPELLED) != 0) {
262            tp.setUnderlineText(mMisspelledUnderlineColor, mMisspelledUnderlineThickness);
263        } else if ((mFlags & FLAG_EASY_CORRECT) != 0) {
264            tp.setUnderlineText(mEasyCorrectUnderlineColor, mEasyCorrectUnderlineThickness);
265        }
266    }
267
268    /**
269     * @return The color of the underline for that span, or 0 if there is no underline
270     *
271     * @hide
272     */
273    public int getUnderlineColor() {
274        // The order here should match what is used in updateDrawState
275        if ((mFlags & FLAG_MISSPELLED) != 0) return mMisspelledUnderlineColor;
276        if ((mFlags & FLAG_EASY_CORRECT) != 0) return mEasyCorrectUnderlineColor;
277        return 0;
278    }
279}
280