SuggestionSpan.java revision c5436f204ba938219223a57c83586aba543f6888
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    /**
60     * Sets this flag if the auto correction is about to be applied to a word/text
61     * that the user is typing/composing. This type of suggestion is rendered differently
62     * to indicate the auto correction is happening.
63     */
64    public static final int FLAG_AUTO_CORRECTION = 0x0004;
65
66    public static final String ACTION_SUGGESTION_PICKED = "android.text.style.SUGGESTION_PICKED";
67    public static final String SUGGESTION_SPAN_PICKED_AFTER = "after";
68    public static final String SUGGESTION_SPAN_PICKED_BEFORE = "before";
69    public static final String SUGGESTION_SPAN_PICKED_HASHCODE = "hashcode";
70
71    public static final int SUGGESTIONS_MAX_SIZE = 5;
72
73    /*
74     * TODO: Needs to check the validity and add a feature that TextView will change
75     * the current IME to the other IME which is specified in SuggestionSpan.
76     * An IME needs to set the span by specifying the target IME and Subtype of SuggestionSpan.
77     * And the current IME might want to specify any IME as the target IME including other IMEs.
78     */
79
80    private int mFlags;
81    private final String[] mSuggestions;
82    private final String mLocaleString;
83    private final String mNotificationTargetClassName;
84    private final int mHashCode;
85
86    private float mEasyCorrectUnderlineThickness;
87    private int mEasyCorrectUnderlineColor;
88
89    private float mMisspelledUnderlineThickness;
90    private int mMisspelledUnderlineColor;
91
92    private float mAutoCorrectionUnderlineThickness;
93    private int mAutoCorrectionUnderlineColor;
94
95    /**
96     * @param context Context for the application
97     * @param suggestions Suggestions for the string under the span
98     * @param flags Additional flags indicating how this span is handled in TextView
99     */
100    public SuggestionSpan(Context context, String[] suggestions, int flags) {
101        this(context, null, suggestions, flags, null);
102    }
103
104    /**
105     * @param locale Locale of the suggestions
106     * @param suggestions Suggestions for the string under the span
107     * @param flags Additional flags indicating how this span is handled in TextView
108     */
109    public SuggestionSpan(Locale locale, String[] suggestions, int flags) {
110        this(null, locale, suggestions, flags, null);
111    }
112
113    /**
114     * @param context Context for the application
115     * @param locale locale Locale of the suggestions
116     * @param suggestions Suggestions for the string under the span. Only the first up to
117     * {@link SuggestionSpan#SUGGESTIONS_MAX_SIZE} will be considered.
118     * @param flags Additional flags indicating how this span is handled in TextView
119     * @param notificationTargetClass if not null, this class will get notified when the user
120     * selects one of the suggestions.
121     */
122    public SuggestionSpan(Context context, Locale locale, String[] suggestions, int flags,
123            Class<?> notificationTargetClass) {
124        final int N = Math.min(SUGGESTIONS_MAX_SIZE, suggestions.length);
125        mSuggestions = Arrays.copyOf(suggestions, N);
126        mFlags = flags;
127        if (context != null && locale == null) {
128            mLocaleString = context.getResources().getConfiguration().locale.toString();
129        } else {
130            mLocaleString = locale.toString();
131        }
132
133        if (notificationTargetClass != null) {
134            mNotificationTargetClassName = notificationTargetClass.getCanonicalName();
135        } else {
136            mNotificationTargetClassName = "";
137        }
138        mHashCode = hashCodeInternal(mSuggestions, mLocaleString, mNotificationTargetClassName);
139
140        initStyle(context);
141    }
142
143    private void initStyle(Context context) {
144        if (context == null) {
145            mMisspelledUnderlineThickness = 0;
146            mEasyCorrectUnderlineThickness = 0;
147            mAutoCorrectionUnderlineThickness = 0;
148            mMisspelledUnderlineColor = Color.BLACK;
149            mEasyCorrectUnderlineColor = Color.BLACK;
150            mAutoCorrectionUnderlineColor = Color.BLACK;
151            return;
152        }
153
154        int defStyle = com.android.internal.R.attr.textAppearanceMisspelledSuggestion;
155        TypedArray typedArray = context.obtainStyledAttributes(
156                null, com.android.internal.R.styleable.SuggestionSpan, defStyle, 0);
157        mMisspelledUnderlineThickness = typedArray.getDimension(
158                com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0);
159        mMisspelledUnderlineColor = typedArray.getColor(
160                com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK);
161
162        defStyle = com.android.internal.R.attr.textAppearanceEasyCorrectSuggestion;
163        typedArray = context.obtainStyledAttributes(
164                null, com.android.internal.R.styleable.SuggestionSpan, defStyle, 0);
165        mEasyCorrectUnderlineThickness = typedArray.getDimension(
166                com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0);
167        mEasyCorrectUnderlineColor = typedArray.getColor(
168                com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK);
169
170        defStyle = com.android.internal.R.attr.textAppearanceAutoCorrectionSuggestion;
171        typedArray = context.obtainStyledAttributes(
172                null, com.android.internal.R.styleable.SuggestionSpan, defStyle, 0);
173        mAutoCorrectionUnderlineThickness = typedArray.getDimension(
174                com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0);
175        mAutoCorrectionUnderlineColor = typedArray.getColor(
176                com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK);
177    }
178
179    public SuggestionSpan(Parcel src) {
180        mSuggestions = src.readStringArray();
181        mFlags = src.readInt();
182        mLocaleString = src.readString();
183        mNotificationTargetClassName = src.readString();
184        mHashCode = src.readInt();
185        mEasyCorrectUnderlineColor = src.readInt();
186        mEasyCorrectUnderlineThickness = src.readFloat();
187        mMisspelledUnderlineColor = src.readInt();
188        mMisspelledUnderlineThickness = src.readFloat();
189        mAutoCorrectionUnderlineColor = src.readInt();
190        mAutoCorrectionUnderlineThickness = src.readFloat();
191    }
192
193    /**
194     * @return an array of suggestion texts for this span
195     */
196    public String[] getSuggestions() {
197        return mSuggestions;
198    }
199
200    /**
201     * @return the locale of the suggestions
202     */
203    public String getLocale() {
204        return mLocaleString;
205    }
206
207    /**
208     * @return The name of the class to notify. The class of the original IME package will receive
209     * a notification when the user selects one of the suggestions. The notification will include
210     * the original string, the suggested replacement string as well as the hashCode of this span.
211     * The class will get notified by an intent that has those information.
212     * This is an internal API because only the framework should know the class name.
213     *
214     * @hide
215     */
216    public String getNotificationTargetClassName() {
217        return mNotificationTargetClassName;
218    }
219
220    public int getFlags() {
221        return mFlags;
222    }
223
224    public void setFlags(int flags) {
225        mFlags = flags;
226    }
227
228    @Override
229    public int describeContents() {
230        return 0;
231    }
232
233    @Override
234    public void writeToParcel(Parcel dest, int flags) {
235        dest.writeStringArray(mSuggestions);
236        dest.writeInt(mFlags);
237        dest.writeString(mLocaleString);
238        dest.writeString(mNotificationTargetClassName);
239        dest.writeInt(mHashCode);
240        dest.writeInt(mEasyCorrectUnderlineColor);
241        dest.writeFloat(mEasyCorrectUnderlineThickness);
242        dest.writeInt(mMisspelledUnderlineColor);
243        dest.writeFloat(mMisspelledUnderlineThickness);
244        dest.writeInt(mAutoCorrectionUnderlineColor);
245        dest.writeFloat(mAutoCorrectionUnderlineThickness);
246    }
247
248    @Override
249    public int getSpanTypeId() {
250        return TextUtils.SUGGESTION_SPAN;
251    }
252
253    @Override
254    public boolean equals(Object o) {
255        if (o instanceof SuggestionSpan) {
256            return ((SuggestionSpan)o).hashCode() == mHashCode;
257        }
258        return false;
259    }
260
261    @Override
262    public int hashCode() {
263        return mHashCode;
264    }
265
266    private static int hashCodeInternal(String[] suggestions, String locale,
267            String notificationTargetClassName) {
268        return Arrays.hashCode(new Object[] {Long.valueOf(SystemClock.uptimeMillis()), suggestions,
269                locale, notificationTargetClassName});
270    }
271
272    public static final Parcelable.Creator<SuggestionSpan> CREATOR =
273            new Parcelable.Creator<SuggestionSpan>() {
274        @Override
275        public SuggestionSpan createFromParcel(Parcel source) {
276            return new SuggestionSpan(source);
277        }
278
279        @Override
280        public SuggestionSpan[] newArray(int size) {
281            return new SuggestionSpan[size];
282        }
283    };
284
285    @Override
286    public void updateDrawState(TextPaint tp) {
287        final boolean misspelled = (mFlags & FLAG_MISSPELLED) != 0;
288        final boolean easy = (mFlags & FLAG_EASY_CORRECT) != 0;
289        final boolean autoCorrection = (mFlags & FLAG_AUTO_CORRECTION) != 0;
290        if (easy) {
291            if (!misspelled) {
292                tp.setUnderlineText(mEasyCorrectUnderlineColor, mEasyCorrectUnderlineThickness);
293            } else if (tp.underlineColor == 0) {
294                // Spans are rendered in an arbitrary order. Since misspelled is less prioritary
295                // than just easy, do not apply misspelled if an easy (or a mispelled) has been set
296                tp.setUnderlineText(mMisspelledUnderlineColor, mMisspelledUnderlineThickness);
297            }
298        } else if (autoCorrection) {
299            tp.setUnderlineText(mAutoCorrectionUnderlineColor, mAutoCorrectionUnderlineThickness);
300        }
301    }
302
303    /**
304     * @return The color of the underline for that span, or 0 if there is no underline
305     *
306     * @hide
307     */
308    public int getUnderlineColor() {
309        // The order here should match what is used in updateDrawState
310        final boolean misspelled = (mFlags & FLAG_MISSPELLED) != 0;
311        final boolean easy = (mFlags & FLAG_EASY_CORRECT) != 0;
312        final boolean autoCorrection = (mFlags & FLAG_AUTO_CORRECTION) != 0;
313        if (easy) {
314            if (!misspelled) {
315                return mEasyCorrectUnderlineColor;
316            } else {
317                return mMisspelledUnderlineColor;
318            }
319        } else if (autoCorrection) {
320            return mAutoCorrectionUnderlineColor;
321        }
322        return 0;
323    }
324}
325