SuggestionSpan.java revision 617feb99a06e7ffb3894e86a286bf30e085f321a
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.Intent;
21import android.content.res.TypedArray;
22import android.graphics.Color;
23import android.os.Parcel;
24import android.os.Parcelable;
25import android.os.SystemClock;
26import android.text.ParcelableSpan;
27import android.text.TextPaint;
28import android.text.TextUtils;
29import android.util.Log;
30import android.view.inputmethod.InputMethodManager;
31import android.widget.TextView;
32
33import java.util.Arrays;
34import java.util.Locale;
35
36/**
37 * Holds suggestion candidates for the text enclosed in this span.
38 *
39 * When such a span is edited in an EditText, double tapping on the text enclosed in this span will
40 * display a popup dialog listing suggestion replacement for that text. The user can then replace
41 * the original text by one of the suggestions.
42 *
43 * These spans should typically be created by the input method to provide correction and alternates
44 * for the text.
45 *
46 * @see TextView#isSuggestionsEnabled()
47 */
48public class SuggestionSpan extends CharacterStyle implements ParcelableSpan {
49
50    private static final String TAG = "SuggestionSpan";
51
52    /**
53     * Sets this flag if the suggestions should be easily accessible with few interactions.
54     * This flag should be set for every suggestions that the user is likely to use.
55     */
56    public static final int FLAG_EASY_CORRECT = 0x0001;
57
58    /**
59     * Sets this flag if the suggestions apply to a misspelled word/text. This type of suggestion is
60     * rendered differently to highlight the error.
61     */
62    public static final int FLAG_MISSPELLED = 0x0002;
63
64    /**
65     * Sets this flag if the auto correction is about to be applied to a word/text
66     * that the user is typing/composing. This type of suggestion is rendered differently
67     * to indicate the auto correction is happening.
68     */
69    public static final int FLAG_AUTO_CORRECTION = 0x0004;
70
71    public static final String ACTION_SUGGESTION_PICKED = "android.text.style.SUGGESTION_PICKED";
72    public static final String SUGGESTION_SPAN_PICKED_AFTER = "after";
73    public static final String SUGGESTION_SPAN_PICKED_BEFORE = "before";
74    public static final String SUGGESTION_SPAN_PICKED_HASHCODE = "hashcode";
75
76    public static final int SUGGESTIONS_MAX_SIZE = 5;
77
78    /*
79     * TODO: Needs to check the validity and add a feature that TextView will change
80     * the current IME to the other IME which is specified in SuggestionSpan.
81     * An IME needs to set the span by specifying the target IME and Subtype of SuggestionSpan.
82     * And the current IME might want to specify any IME as the target IME including other IMEs.
83     */
84
85    private int mFlags;
86    private final String[] mSuggestions;
87    private final String mLocaleString;
88    private final String mNotificationTargetClassName;
89    private final String mNotificationTargetPackageName;
90    private final int mHashCode;
91
92    private float mEasyCorrectUnderlineThickness;
93    private int mEasyCorrectUnderlineColor;
94
95    private float mMisspelledUnderlineThickness;
96    private int mMisspelledUnderlineColor;
97
98    private float mAutoCorrectionUnderlineThickness;
99    private int mAutoCorrectionUnderlineColor;
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. Null values not permitted.
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 (locale != null) {
134            mLocaleString = locale.toString();
135        } else if (context != null) {
136            mLocaleString = context.getResources().getConfiguration().locale.toString();
137        } else {
138            Log.e("SuggestionSpan", "No locale or context specified in SuggestionSpan constructor");
139            mLocaleString = "";
140        }
141
142        if (context != null) {
143            mNotificationTargetPackageName = context.getPackageName();
144        } else {
145            mNotificationTargetPackageName = null;
146        }
147
148        if (notificationTargetClass != null) {
149            mNotificationTargetClassName = notificationTargetClass.getCanonicalName();
150        } else {
151            mNotificationTargetClassName = "";
152        }
153        mHashCode = hashCodeInternal(mSuggestions, mLocaleString, mNotificationTargetClassName);
154
155        initStyle(context);
156    }
157
158    private void initStyle(Context context) {
159        if (context == null) {
160            mMisspelledUnderlineThickness = 0;
161            mEasyCorrectUnderlineThickness = 0;
162            mAutoCorrectionUnderlineThickness = 0;
163            mMisspelledUnderlineColor = Color.BLACK;
164            mEasyCorrectUnderlineColor = Color.BLACK;
165            mAutoCorrectionUnderlineColor = Color.BLACK;
166            return;
167        }
168
169        int defStyleAttr = com.android.internal.R.attr.textAppearanceMisspelledSuggestion;
170        TypedArray typedArray = context.obtainStyledAttributes(
171                null, com.android.internal.R.styleable.SuggestionSpan, defStyleAttr, 0);
172        mMisspelledUnderlineThickness = typedArray.getDimension(
173                com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0);
174        mMisspelledUnderlineColor = typedArray.getColor(
175                com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK);
176
177        defStyleAttr = com.android.internal.R.attr.textAppearanceEasyCorrectSuggestion;
178        typedArray = context.obtainStyledAttributes(
179                null, com.android.internal.R.styleable.SuggestionSpan, defStyleAttr, 0);
180        mEasyCorrectUnderlineThickness = typedArray.getDimension(
181                com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0);
182        mEasyCorrectUnderlineColor = typedArray.getColor(
183                com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK);
184
185        defStyleAttr = com.android.internal.R.attr.textAppearanceAutoCorrectionSuggestion;
186        typedArray = context.obtainStyledAttributes(
187                null, com.android.internal.R.styleable.SuggestionSpan, defStyleAttr, 0);
188        mAutoCorrectionUnderlineThickness = typedArray.getDimension(
189                com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0);
190        mAutoCorrectionUnderlineColor = typedArray.getColor(
191                com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK);
192    }
193
194    public SuggestionSpan(Parcel src) {
195        mSuggestions = src.readStringArray();
196        mFlags = src.readInt();
197        mLocaleString = src.readString();
198        mNotificationTargetClassName = src.readString();
199        mNotificationTargetPackageName = src.readString();
200        mHashCode = src.readInt();
201        mEasyCorrectUnderlineColor = src.readInt();
202        mEasyCorrectUnderlineThickness = src.readFloat();
203        mMisspelledUnderlineColor = src.readInt();
204        mMisspelledUnderlineThickness = src.readFloat();
205        mAutoCorrectionUnderlineColor = src.readInt();
206        mAutoCorrectionUnderlineThickness = src.readFloat();
207    }
208
209    /**
210     * @return an array of suggestion texts for this span
211     */
212    public String[] getSuggestions() {
213        return mSuggestions;
214    }
215
216    /**
217     * @return the locale of the suggestions
218     */
219    public String getLocale() {
220        return mLocaleString;
221    }
222
223    /**
224     * @return The name of the class to notify. The class of the original IME package will receive
225     * a notification when the user selects one of the suggestions. The notification will include
226     * the original string, the suggested replacement string as well as the hashCode of this span.
227     * The class will get notified by an intent that has those information.
228     * This is an internal API because only the framework should know the class name.
229     *
230     * @hide
231     */
232    public String getNotificationTargetClassName() {
233        return mNotificationTargetClassName;
234    }
235
236    public int getFlags() {
237        return mFlags;
238    }
239
240    public void setFlags(int flags) {
241        mFlags = flags;
242    }
243
244    @Override
245    public int describeContents() {
246        return 0;
247    }
248
249    @Override
250    public void writeToParcel(Parcel dest, int flags) {
251        dest.writeStringArray(mSuggestions);
252        dest.writeInt(mFlags);
253        dest.writeString(mLocaleString);
254        dest.writeString(mNotificationTargetClassName);
255        dest.writeString(mNotificationTargetPackageName);
256        dest.writeInt(mHashCode);
257        dest.writeInt(mEasyCorrectUnderlineColor);
258        dest.writeFloat(mEasyCorrectUnderlineThickness);
259        dest.writeInt(mMisspelledUnderlineColor);
260        dest.writeFloat(mMisspelledUnderlineThickness);
261        dest.writeInt(mAutoCorrectionUnderlineColor);
262        dest.writeFloat(mAutoCorrectionUnderlineThickness);
263    }
264
265    @Override
266    public int getSpanTypeId() {
267        return TextUtils.SUGGESTION_SPAN;
268    }
269
270    @Override
271    public boolean equals(Object o) {
272        if (o instanceof SuggestionSpan) {
273            return ((SuggestionSpan)o).hashCode() == mHashCode;
274        }
275        return false;
276    }
277
278    @Override
279    public int hashCode() {
280        return mHashCode;
281    }
282
283    private static int hashCodeInternal(String[] suggestions, String locale,
284            String notificationTargetClassName) {
285        return Arrays.hashCode(new Object[] {Long.valueOf(SystemClock.uptimeMillis()), suggestions,
286                locale, notificationTargetClassName});
287    }
288
289    public static final Parcelable.Creator<SuggestionSpan> CREATOR =
290            new Parcelable.Creator<SuggestionSpan>() {
291        @Override
292        public SuggestionSpan createFromParcel(Parcel source) {
293            return new SuggestionSpan(source);
294        }
295
296        @Override
297        public SuggestionSpan[] newArray(int size) {
298            return new SuggestionSpan[size];
299        }
300    };
301
302    @Override
303    public void updateDrawState(TextPaint tp) {
304        final boolean misspelled = (mFlags & FLAG_MISSPELLED) != 0;
305        final boolean easy = (mFlags & FLAG_EASY_CORRECT) != 0;
306        final boolean autoCorrection = (mFlags & FLAG_AUTO_CORRECTION) != 0;
307        if (easy) {
308            if (!misspelled) {
309                tp.setUnderlineText(mEasyCorrectUnderlineColor, mEasyCorrectUnderlineThickness);
310            } else if (tp.underlineColor == 0) {
311                // Spans are rendered in an arbitrary order. Since misspelled is less prioritary
312                // than just easy, do not apply misspelled if an easy (or a mispelled) has been set
313                tp.setUnderlineText(mMisspelledUnderlineColor, mMisspelledUnderlineThickness);
314            }
315        } else if (autoCorrection) {
316            tp.setUnderlineText(mAutoCorrectionUnderlineColor, mAutoCorrectionUnderlineThickness);
317        }
318    }
319
320    /**
321     * @return The color of the underline for that span, or 0 if there is no underline
322     *
323     * @hide
324     */
325    public int getUnderlineColor() {
326        // The order here should match what is used in updateDrawState
327        final boolean misspelled = (mFlags & FLAG_MISSPELLED) != 0;
328        final boolean easy = (mFlags & FLAG_EASY_CORRECT) != 0;
329        final boolean autoCorrection = (mFlags & FLAG_AUTO_CORRECTION) != 0;
330        if (easy) {
331            if (!misspelled) {
332                return mEasyCorrectUnderlineColor;
333            } else {
334                return mMisspelledUnderlineColor;
335            }
336        } else if (autoCorrection) {
337            return mAutoCorrectionUnderlineColor;
338        }
339        return 0;
340    }
341
342    /**
343     * Notifies a suggestion selection.
344     *
345     * @hide
346     */
347    public void notifySelection(Context context, String original, int index) {
348        final Intent intent = new Intent();
349
350        if (context == null || mNotificationTargetClassName == null) {
351            return;
352        }
353        // Ensures that only a class in the original IME package will receive the
354        // notification.
355        if (mSuggestions == null || index < 0 || index >= mSuggestions.length) {
356            Log.w(TAG, "Unable to notify the suggestion as the index is out of range index=" + index
357                    + " length=" + mSuggestions.length);
358            return;
359        }
360
361        // The package name is not mandatory (legacy from JB), and if the package name
362        // is missing, we try to notify the suggestion through the input method manager.
363        if (mNotificationTargetPackageName != null) {
364            intent.setClassName(mNotificationTargetPackageName, mNotificationTargetClassName);
365            intent.setAction(SuggestionSpan.ACTION_SUGGESTION_PICKED);
366            intent.putExtra(SuggestionSpan.SUGGESTION_SPAN_PICKED_BEFORE, original);
367            intent.putExtra(SuggestionSpan.SUGGESTION_SPAN_PICKED_AFTER, mSuggestions[index]);
368            intent.putExtra(SuggestionSpan.SUGGESTION_SPAN_PICKED_HASHCODE, hashCode());
369            context.sendBroadcast(intent);
370        } else {
371            InputMethodManager imm = InputMethodManager.peekInstance();
372            if (imm != null) {
373                imm.notifySuggestionPicked(this, original, index);
374            }
375        }
376    }
377}
378