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