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         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
getSpanTypeId()266     public int getSpanTypeId() {
267         return TextUtils.SUGGESTION_SPAN;
268     }
269 
270     @Override
equals(Object o)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
hashCode()279     public int hashCode() {
280         return mHashCode;
281     }
282 
hashCodeInternal(String[] suggestions, String locale, String notificationTargetClassName)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
updateDrawState(TextPaint tp)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      */
getUnderlineColor()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      */
notifySelection(Context context, String original, int index)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