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