SuggestionSpan.java revision 7d1c55fad8194e2c82f5f88f98ab5569f9c52ab9
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#setSuggestionsEnabled(boolean)
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    /**
65     * The default underline thickness as a percentage of the system's default underline thickness
66     * (i.e., 100 means the default thickness, and 200 is a double thickness).
67     */
68    private static final int DEFAULT_UNDERLINE_PERCENTAGE = 100;
69
70    public static final int SUGGESTIONS_MAX_SIZE = 5;
71
72    /*
73     * TODO: Needs to check the validity and add a feature that TextView will change
74     * the current IME to the other IME which is specified in SuggestionSpan.
75     * An IME needs to set the span by specifying the target IME and Subtype of SuggestionSpan.
76     * And the current IME might want to specify any IME as the target IME including other IMEs.
77     */
78
79    private final int mFlags;
80    private final String[] mSuggestions;
81    private final String mLocaleString;
82    private final String mNotificationTargetClassName;
83    private final int mHashCode;
84
85    private float mMisspelledUnderlineThickness;
86    private int mMisspelledUnderlineColor;
87    private float mEasyCorrectUnderlineThickness;
88    private int mEasyCorrectUnderlineColor;
89
90    /*
91     * TODO: If switching IME is required, needs to add parameters for ids of InputMethodInfo
92     * and InputMethodSubtype.
93     */
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
117     * @param flags Additional flags indicating how this span is handled in TextView
118     * @param notificationTargetClass if not null, this class will get notified when the user
119     * selects one of the suggestions.
120     */
121    public SuggestionSpan(Context context, Locale locale, String[] suggestions, int flags,
122            Class<?> notificationTargetClass) {
123        final int N = Math.min(SUGGESTIONS_MAX_SIZE, suggestions.length);
124        mSuggestions = Arrays.copyOf(suggestions, N);
125        mFlags = flags;
126        if (context != null && locale == null) {
127            mLocaleString = context.getResources().getConfiguration().locale.toString();
128        } else {
129            mLocaleString = locale.toString();
130        }
131
132        if (notificationTargetClass != null) {
133            mNotificationTargetClassName = notificationTargetClass.getCanonicalName();
134        } else {
135            mNotificationTargetClassName = "";
136        }
137        mHashCode = hashCodeInternal(
138                mFlags, mSuggestions, mLocaleString, mNotificationTargetClassName);
139
140        initStyle(context);
141    }
142
143    private void initStyle(Context context) {
144        // Read the colors. We need to store the color and the underline thickness, as the span
145        // does not have access to the context when it is read from a parcel.
146        TypedArray typedArray;
147
148        typedArray = context.obtainStyledAttributes(null,
149                com.android.internal.R.styleable.SuggestionSpan,
150                com.android.internal.R.attr.textAppearanceEasyCorrectSuggestion, 0);
151
152        mEasyCorrectUnderlineThickness = getThicknessPercentage(typedArray,
153                com.android.internal.R.styleable.SuggestionSpan_textUnderlineThicknessPercentage);
154        mEasyCorrectUnderlineColor = typedArray.getColor(
155                com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK);
156
157        typedArray = context.obtainStyledAttributes(null,
158                com.android.internal.R.styleable.SuggestionSpan,
159                com.android.internal.R.attr.textAppearanceMisspelledSuggestion, 0);
160        mMisspelledUnderlineThickness = getThicknessPercentage(typedArray,
161                com.android.internal.R.styleable.SuggestionSpan_textUnderlineThicknessPercentage);
162        mMisspelledUnderlineColor = typedArray.getColor(
163                com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK);
164    }
165
166    private static float getThicknessPercentage(TypedArray typedArray, int index) {
167        int value  = typedArray.getInteger(index, DEFAULT_UNDERLINE_PERCENTAGE);
168        return value / 100.0f;
169    }
170
171    public SuggestionSpan(Parcel src) {
172        mSuggestions = src.readStringArray();
173        mFlags = src.readInt();
174        mLocaleString = src.readString();
175        mNotificationTargetClassName = src.readString();
176        mHashCode = src.readInt();
177        mEasyCorrectUnderlineColor = src.readInt();
178        mEasyCorrectUnderlineThickness = src.readFloat();
179        mMisspelledUnderlineColor = src.readInt();
180        mMisspelledUnderlineThickness = src.readFloat();
181    }
182
183    /**
184     * @return an array of suggestion texts for this span
185     */
186    public String[] getSuggestions() {
187        return mSuggestions;
188    }
189
190    /**
191     * @return the locale of the suggestions
192     */
193    public String getLocale() {
194        return mLocaleString;
195    }
196
197    /**
198     * @return The name of the class to notify. The class of the original IME package will receive
199     * a notification when the user selects one of the suggestions. The notification will include
200     * the original string, the suggested replacement string as well as the hashCode of this span.
201     * The class will get notified by an intent that has those information.
202     * This is an internal API because only the framework should know the class name.
203     *
204     * @hide
205     */
206    public String getNotificationTargetClassName() {
207        return mNotificationTargetClassName;
208    }
209
210    public int getFlags() {
211        return mFlags;
212    }
213
214    @Override
215    public int describeContents() {
216        return 0;
217    }
218
219    @Override
220    public void writeToParcel(Parcel dest, int flags) {
221        dest.writeStringArray(mSuggestions);
222        dest.writeInt(mFlags);
223        dest.writeString(mLocaleString);
224        dest.writeString(mNotificationTargetClassName);
225        dest.writeInt(mHashCode);
226        dest.writeInt(mEasyCorrectUnderlineColor);
227        dest.writeFloat(mEasyCorrectUnderlineThickness);
228        dest.writeInt(mMisspelledUnderlineColor);
229        dest.writeFloat(mMisspelledUnderlineThickness);
230    }
231
232    @Override
233    public int getSpanTypeId() {
234        return TextUtils.SUGGESTION_SPAN;
235    }
236
237    @Override
238    public boolean equals(Object o) {
239        if (o instanceof SuggestionSpan) {
240            return ((SuggestionSpan)o).hashCode() == mHashCode;
241        }
242        return false;
243    }
244
245    @Override
246    public int hashCode() {
247        return mHashCode;
248    }
249
250    private static int hashCodeInternal(int flags, String[] suggestions,String locale,
251            String notificationTargetClassName) {
252        return Arrays.hashCode(new Object[] {SystemClock.uptimeMillis(), flags, suggestions, locale,
253                notificationTargetClassName});
254    }
255
256    public static final Parcelable.Creator<SuggestionSpan> CREATOR =
257            new Parcelable.Creator<SuggestionSpan>() {
258        @Override
259        public SuggestionSpan createFromParcel(Parcel source) {
260            return new SuggestionSpan(source);
261        }
262
263        @Override
264        public SuggestionSpan[] newArray(int size) {
265            return new SuggestionSpan[size];
266        }
267    };
268
269    @Override
270    public void updateDrawState(TextPaint tp) {
271        if ((getFlags() & FLAG_MISSPELLED) != 0) {
272            tp.setUnderlineText(true, mMisspelledUnderlineColor, mMisspelledUnderlineThickness);
273        } else if ((getFlags() & FLAG_EASY_CORRECT) != 0) {
274            tp.setUnderlineText(true, mEasyCorrectUnderlineColor, mEasyCorrectUnderlineThickness);
275        }
276    }
277}
278