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