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.util.Log;
29import android.widget.TextView;
30
31import java.util.Arrays;
32import java.util.Locale;
33
34/**
35 * Holds suggestion candidates for the text enclosed in this span.
36 *
37 * When such a span is edited in an EditText, double tapping on the text enclosed in this span will
38 * display a popup dialog listing suggestion replacement for that text. The user can then replace
39 * the original text by one of the suggestions.
40 *
41 * These spans should typically be created by the input method to provide correction and alternates
42 * for the text.
43 *
44 * @see TextView#isSuggestionsEnabled()
45 */
46public class SuggestionSpan extends CharacterStyle implements ParcelableSpan {
47
48    /**
49     * Sets this flag if the suggestions should be easily accessible with few interactions.
50     * This flag should be set for every suggestions that the user is likely to use.
51     */
52    public static final int FLAG_EASY_CORRECT = 0x0001;
53
54    /**
55     * Sets this flag if the suggestions apply to a misspelled word/text. This type of suggestion is
56     * rendered differently to highlight the error.
57     */
58    public static final int FLAG_MISSPELLED = 0x0002;
59
60    /**
61     * Sets this flag if the auto correction is about to be applied to a word/text
62     * that the user is typing/composing. This type of suggestion is rendered differently
63     * to indicate the auto correction is happening.
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     * @param context Context for the application
98     * @param suggestions Suggestions for the string under the span
99     * @param flags Additional flags indicating how this span is handled in TextView
100     */
101    public SuggestionSpan(Context context, String[] suggestions, int flags) {
102        this(context, null, suggestions, flags, null);
103    }
104
105    /**
106     * @param locale Locale of the suggestions
107     * @param suggestions Suggestions for the string under the span
108     * @param flags Additional flags indicating how this span is handled in TextView
109     */
110    public SuggestionSpan(Locale locale, String[] suggestions, int flags) {
111        this(null, locale, suggestions, flags, null);
112    }
113
114    /**
115     * @param context Context for the application
116     * @param locale locale Locale of the suggestions
117     * @param suggestions Suggestions for the string under the span. Only the first up to
118     * {@link SuggestionSpan#SUGGESTIONS_MAX_SIZE} will be considered. Null values not permitted.
119     * @param flags Additional flags indicating how this span is handled in TextView
120     * @param notificationTargetClass if not null, this class will get notified when the user
121     * selects one of the suggestions.
122     */
123    public SuggestionSpan(Context context, Locale locale, String[] suggestions, int flags,
124            Class<?> notificationTargetClass) {
125        final int N = Math.min(SUGGESTIONS_MAX_SIZE, suggestions.length);
126        mSuggestions = Arrays.copyOf(suggestions, N);
127        mFlags = flags;
128        if (locale != null) {
129            mLocaleString = locale.toString();
130        } else if (context != null) {
131            mLocaleString = context.getResources().getConfiguration().locale.toString();
132        } else {
133            Log.e("SuggestionSpan", "No locale or context specified in SuggestionSpan constructor");
134            mLocaleString = "";
135        }
136
137        if (notificationTargetClass != null) {
138            mNotificationTargetClassName = notificationTargetClass.getCanonicalName();
139        } else {
140            mNotificationTargetClassName = "";
141        }
142        mHashCode = hashCodeInternal(mSuggestions, mLocaleString, mNotificationTargetClassName);
143
144        initStyle(context);
145    }
146
147    private void initStyle(Context context) {
148        if (context == null) {
149            mMisspelledUnderlineThickness = 0;
150            mEasyCorrectUnderlineThickness = 0;
151            mAutoCorrectionUnderlineThickness = 0;
152            mMisspelledUnderlineColor = Color.BLACK;
153            mEasyCorrectUnderlineColor = Color.BLACK;
154            mAutoCorrectionUnderlineColor = Color.BLACK;
155            return;
156        }
157
158        int defStyle = com.android.internal.R.attr.textAppearanceMisspelledSuggestion;
159        TypedArray typedArray = context.obtainStyledAttributes(
160                null, com.android.internal.R.styleable.SuggestionSpan, defStyle, 0);
161        mMisspelledUnderlineThickness = typedArray.getDimension(
162                com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0);
163        mMisspelledUnderlineColor = typedArray.getColor(
164                com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK);
165
166        defStyle = com.android.internal.R.attr.textAppearanceEasyCorrectSuggestion;
167        typedArray = context.obtainStyledAttributes(
168                null, com.android.internal.R.styleable.SuggestionSpan, defStyle, 0);
169        mEasyCorrectUnderlineThickness = typedArray.getDimension(
170                com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0);
171        mEasyCorrectUnderlineColor = typedArray.getColor(
172                com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK);
173
174        defStyle = com.android.internal.R.attr.textAppearanceAutoCorrectionSuggestion;
175        typedArray = context.obtainStyledAttributes(
176                null, com.android.internal.R.styleable.SuggestionSpan, defStyle, 0);
177        mAutoCorrectionUnderlineThickness = typedArray.getDimension(
178                com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0);
179        mAutoCorrectionUnderlineColor = typedArray.getColor(
180                com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK);
181    }
182
183    public SuggestionSpan(Parcel src) {
184        mSuggestions = src.readStringArray();
185        mFlags = src.readInt();
186        mLocaleString = src.readString();
187        mNotificationTargetClassName = src.readString();
188        mHashCode = src.readInt();
189        mEasyCorrectUnderlineColor = src.readInt();
190        mEasyCorrectUnderlineThickness = src.readFloat();
191        mMisspelledUnderlineColor = src.readInt();
192        mMisspelledUnderlineThickness = src.readFloat();
193        mAutoCorrectionUnderlineColor = src.readInt();
194        mAutoCorrectionUnderlineThickness = src.readFloat();
195    }
196
197    /**
198     * @return an array of suggestion texts for this span
199     */
200    public String[] getSuggestions() {
201        return mSuggestions;
202    }
203
204    /**
205     * @return the locale of the suggestions
206     */
207    public String getLocale() {
208        return mLocaleString;
209    }
210
211    /**
212     * @return The name of the class to notify. The class of the original IME package will receive
213     * a notification when the user selects one of the suggestions. The notification will include
214     * the original string, the suggested replacement string as well as the hashCode of this span.
215     * The class will get notified by an intent that has those information.
216     * This is an internal API because only the framework should know the class name.
217     *
218     * @hide
219     */
220    public String getNotificationTargetClassName() {
221        return mNotificationTargetClassName;
222    }
223
224    public int getFlags() {
225        return mFlags;
226    }
227
228    public void setFlags(int flags) {
229        mFlags = flags;
230    }
231
232    @Override
233    public int describeContents() {
234        return 0;
235    }
236
237    @Override
238    public void writeToParcel(Parcel dest, int flags) {
239        dest.writeStringArray(mSuggestions);
240        dest.writeInt(mFlags);
241        dest.writeString(mLocaleString);
242        dest.writeString(mNotificationTargetClassName);
243        dest.writeInt(mHashCode);
244        dest.writeInt(mEasyCorrectUnderlineColor);
245        dest.writeFloat(mEasyCorrectUnderlineThickness);
246        dest.writeInt(mMisspelledUnderlineColor);
247        dest.writeFloat(mMisspelledUnderlineThickness);
248        dest.writeInt(mAutoCorrectionUnderlineColor);
249        dest.writeFloat(mAutoCorrectionUnderlineThickness);
250    }
251
252    @Override
253    public int getSpanTypeId() {
254        return TextUtils.SUGGESTION_SPAN;
255    }
256
257    @Override
258    public boolean equals(Object o) {
259        if (o instanceof SuggestionSpan) {
260            return ((SuggestionSpan)o).hashCode() == mHashCode;
261        }
262        return false;
263    }
264
265    @Override
266    public int hashCode() {
267        return mHashCode;
268    }
269
270    private static int hashCodeInternal(String[] suggestions, String locale,
271            String notificationTargetClassName) {
272        return Arrays.hashCode(new Object[] {Long.valueOf(SystemClock.uptimeMillis()), suggestions,
273                locale, notificationTargetClassName});
274    }
275
276    public static final Parcelable.Creator<SuggestionSpan> CREATOR =
277            new Parcelable.Creator<SuggestionSpan>() {
278        @Override
279        public SuggestionSpan createFromParcel(Parcel source) {
280            return new SuggestionSpan(source);
281        }
282
283        @Override
284        public SuggestionSpan[] newArray(int size) {
285            return new SuggestionSpan[size];
286        }
287    };
288
289    @Override
290    public void updateDrawState(TextPaint tp) {
291        final boolean misspelled = (mFlags & FLAG_MISSPELLED) != 0;
292        final boolean easy = (mFlags & FLAG_EASY_CORRECT) != 0;
293        final boolean autoCorrection = (mFlags & FLAG_AUTO_CORRECTION) != 0;
294        if (easy) {
295            if (!misspelled) {
296                tp.setUnderlineText(mEasyCorrectUnderlineColor, mEasyCorrectUnderlineThickness);
297            } else if (tp.underlineColor == 0) {
298                // Spans are rendered in an arbitrary order. Since misspelled is less prioritary
299                // than just easy, do not apply misspelled if an easy (or a mispelled) has been set
300                tp.setUnderlineText(mMisspelledUnderlineColor, mMisspelledUnderlineThickness);
301            }
302        } else if (autoCorrection) {
303            tp.setUnderlineText(mAutoCorrectionUnderlineColor, mAutoCorrectionUnderlineThickness);
304        }
305    }
306
307    /**
308     * @return The color of the underline for that span, or 0 if there is no underline
309     *
310     * @hide
311     */
312    public int getUnderlineColor() {
313        // The order here should match what is used in updateDrawState
314        final boolean misspelled = (mFlags & FLAG_MISSPELLED) != 0;
315        final boolean easy = (mFlags & FLAG_EASY_CORRECT) != 0;
316        final boolean autoCorrection = (mFlags & FLAG_AUTO_CORRECTION) != 0;
317        if (easy) {
318            if (!misspelled) {
319                return mEasyCorrectUnderlineColor;
320            } else {
321                return mMisspelledUnderlineColor;
322            }
323        } else if (autoCorrection) {
324            return mAutoCorrectionUnderlineColor;
325        }
326        return 0;
327    }
328}
329