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 
17 package android.text.style;
18 
19 import android.content.Context;
20 import android.content.Intent;
21 import android.content.res.TypedArray;
22 import android.graphics.Color;
23 import android.os.Parcel;
24 import android.os.Parcelable;
25 import android.os.SystemClock;
26 import android.text.ParcelableSpan;
27 import android.text.TextPaint;
28 import android.text.TextUtils;
29 import android.util.Log;
30 import android.view.inputmethod.InputMethodManager;
31 import android.widget.TextView;
32 
33 import java.util.Arrays;
34 import 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  */
48 public 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      */
SuggestionSpan(Context context, String[] suggestions, int flags)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      */
SuggestionSpan(Locale locale, String[] suggestions, int flags)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      */
SuggestionSpan(Context context, Locale locale, String[] suggestions, int flags, Class<?> notificationTargetClass)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 
initStyle(Context context)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 
SuggestionSpan(Parcel src)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      */
getSuggestions()212     public String[] getSuggestions() {
213         return mSuggestions;
214     }
215 
216     /**
217      * @return the locale of the suggestions
218      */
getLocale()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      */
getNotificationTargetClassName()232     public String getNotificationTargetClassName() {
233         return mNotificationTargetClassName;
234     }
235 
getFlags()236     public int getFlags() {
237         return mFlags;
238     }
239 
setFlags(int flags)240     public void setFlags(int flags) {
241         mFlags = flags;
242     }
243 
244     @Override
describeContents()245     public int describeContents() {
246         return 0;
247     }
248 
249     @Override
writeToParcel(Parcel dest, int flags)250     public void writeToParcel(Parcel dest, int flags) {
251         writeToParcelInternal(dest, flags);
252     }
253 
254     /** @hide */
writeToParcelInternal(Parcel dest, int flags)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
getSpanTypeId()271     public int getSpanTypeId() {
272         return getSpanTypeIdInternal();
273     }
274 
275     /** @hide */
getSpanTypeIdInternal()276     public int getSpanTypeIdInternal() {
277         return TextUtils.SUGGESTION_SPAN;
278     }
279 
280     @Override
equals(Object o)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
hashCode()289     public int hashCode() {
290         return mHashCode;
291     }
292 
hashCodeInternal(String[] suggestions, String locale, String notificationTargetClassName)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
updateDrawState(TextPaint tp)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      */
getUnderlineColor()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      */
notifySelection(Context context, String original, int index)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