SuggestionSpan.java revision a70d4a90a029910f788c3e7f8715cf3b842b1e2b
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        writeToParcelInternal(dest, flags);
252    }
253
254    /** @hide */
255    public void writeToParcelInternal(Parcel dest, int flags) {
256        dest.writeStringArray(mSuggestions);
257        dest.writeInt(mFlags);
258        dest.writeString(mLocaleString);
259        dest.writeString(mNotificationTargetClassName);
260        dest.writeString(mNotificationTargetPackageName);
261        dest.writeInt(mHashCode);
262        dest.writeInt(mEasyCorrectUnderlineColor);
263        dest.writeFloat(mEasyCorrectUnderlineThickness);
264        dest.writeInt(mMisspelledUnderlineColor);
265        dest.writeFloat(mMisspelledUnderlineThickness);
266        dest.writeInt(mAutoCorrectionUnderlineColor);
267        dest.writeFloat(mAutoCorrectionUnderlineThickness);
268    }
269
270    @Override
271    public int getSpanTypeId() {
272        return getSpanTypeIdInternal();
273    }
274
275    /** @hide */
276    public int getSpanTypeIdInternal() {
277        return TextUtils.SUGGESTION_SPAN;
278    }
279
280    @Override
281    public boolean equals(Object o) {
282        if (o instanceof SuggestionSpan) {
283            return ((SuggestionSpan)o).hashCode() == mHashCode;
284        }
285        return false;
286    }
287
288    @Override
289    public int hashCode() {
290        return mHashCode;
291    }
292
293    private static int hashCodeInternal(String[] suggestions, String locale,
294            String notificationTargetClassName) {
295        return Arrays.hashCode(new Object[] {Long.valueOf(SystemClock.uptimeMillis()), suggestions,
296                locale, notificationTargetClassName});
297    }
298
299    public static final Parcelable.Creator<SuggestionSpan> CREATOR =
300            new Parcelable.Creator<SuggestionSpan>() {
301        @Override
302        public SuggestionSpan createFromParcel(Parcel source) {
303            return new SuggestionSpan(source);
304        }
305
306        @Override
307        public SuggestionSpan[] newArray(int size) {
308            return new SuggestionSpan[size];
309        }
310    };
311
312    @Override
313    public void updateDrawState(TextPaint tp) {
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                tp.setUnderlineText(mEasyCorrectUnderlineColor, mEasyCorrectUnderlineThickness);
320            } else if (tp.underlineColor == 0) {
321                // Spans are rendered in an arbitrary order. Since misspelled is less prioritary
322                // than just easy, do not apply misspelled if an easy (or a mispelled) has been set
323                tp.setUnderlineText(mMisspelledUnderlineColor, mMisspelledUnderlineThickness);
324            }
325        } else if (autoCorrection) {
326            tp.setUnderlineText(mAutoCorrectionUnderlineColor, mAutoCorrectionUnderlineThickness);
327        }
328    }
329
330    /**
331     * @return The color of the underline for that span, or 0 if there is no underline
332     *
333     * @hide
334     */
335    public int getUnderlineColor() {
336        // The order here should match what is used in updateDrawState
337        final boolean misspelled = (mFlags & FLAG_MISSPELLED) != 0;
338        final boolean easy = (mFlags & FLAG_EASY_CORRECT) != 0;
339        final boolean autoCorrection = (mFlags & FLAG_AUTO_CORRECTION) != 0;
340        if (easy) {
341            if (!misspelled) {
342                return mEasyCorrectUnderlineColor;
343            } else {
344                return mMisspelledUnderlineColor;
345            }
346        } else if (autoCorrection) {
347            return mAutoCorrectionUnderlineColor;
348        }
349        return 0;
350    }
351
352    /**
353     * Notifies a suggestion selection.
354     *
355     * @hide
356     */
357    public void notifySelection(Context context, String original, int index) {
358        final Intent intent = new Intent();
359
360        if (context == null || mNotificationTargetClassName == null) {
361            return;
362        }
363        // Ensures that only a class in the original IME package will receive the
364        // notification.
365        if (mSuggestions == null || index < 0 || index >= mSuggestions.length) {
366            Log.w(TAG, "Unable to notify the suggestion as the index is out of range index=" + index
367                    + " length=" + mSuggestions.length);
368            return;
369        }
370
371        // The package name is not mandatory (legacy from JB), and if the package name
372        // is missing, we try to notify the suggestion through the input method manager.
373        if (mNotificationTargetPackageName != null) {
374            intent.setClassName(mNotificationTargetPackageName, mNotificationTargetClassName);
375            intent.setAction(SuggestionSpan.ACTION_SUGGESTION_PICKED);
376            intent.putExtra(SuggestionSpan.SUGGESTION_SPAN_PICKED_BEFORE, original);
377            intent.putExtra(SuggestionSpan.SUGGESTION_SPAN_PICKED_AFTER, mSuggestions[index]);
378            intent.putExtra(SuggestionSpan.SUGGESTION_SPAN_PICKED_HASHCODE, hashCode());
379            context.sendBroadcast(intent);
380        } else {
381            InputMethodManager imm = InputMethodManager.peekInstance();
382            if (imm != null) {
383                imm.notifySuggestionPicked(this, original, index);
384            }
385        }
386    }
387}
388