• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.annotation.ColorInt;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.compat.annotation.UnsupportedAppUsage;
23 import android.content.Context;
24 import android.content.res.TypedArray;
25 import android.graphics.Color;
26 import android.os.Build;
27 import android.os.Parcel;
28 import android.os.Parcelable;
29 import android.os.SystemClock;
30 import android.text.ParcelableSpan;
31 import android.text.TextPaint;
32 import android.text.TextUtils;
33 import android.util.Log;
34 import android.widget.TextView;
35 
36 import java.util.Arrays;
37 import java.util.Locale;
38 
39 /**
40  * Holds suggestion candidates for the text enclosed in this span.
41  *
42  * When such a span is edited in an EditText, double tapping on the text enclosed in this span will
43  * display a popup dialog listing suggestion replacement for that text. The user can then replace
44  * the original text by one of the suggestions.
45  *
46  * These spans should typically be created by the input method to provide correction and alternates
47  * for the text.
48  *
49  * @see TextView#isSuggestionsEnabled()
50  */
51 @android.ravenwood.annotation.RavenwoodKeepWholeClass
52 public class SuggestionSpan extends CharacterStyle implements ParcelableSpan {
53 
54     private static final String TAG = "SuggestionSpan";
55 
56     /**
57      * Sets this flag if the suggestions should be easily accessible with few interactions.
58      * This flag should be set for every suggestions that the user is likely to use.
59      */
60     public static final int FLAG_EASY_CORRECT = 0x0001;
61 
62     /**
63      * Sets this flag if the suggestions apply to a misspelled word/text. This type of suggestion is
64      * rendered differently to highlight the error.
65      */
66     public static final int FLAG_MISSPELLED = 0x0002;
67 
68     /**
69      * Sets this flag if the auto correction is about to be applied to a word/text
70      * that the user is typing/composing. This type of suggestion is rendered differently
71      * to indicate the auto correction is happening.
72      */
73     public static final int FLAG_AUTO_CORRECTION = 0x0004;
74 
75     /**
76      * Sets this flag if the suggestions apply to a grammar error. This type of suggestion is
77      * rendered differently to highlight the error.
78      */
79     public static final int FLAG_GRAMMAR_ERROR = 0x0008;
80 
81     /**
82      * This action is deprecated in {@link android.os.Build.VERSION_CODES#Q}.
83      *
84      * @deprecated For IMEs to receive this kind of user interaction signals, implement IMEs' own
85      *             suggestion picker UI instead of relying on {@link SuggestionSpan}. To retrieve
86      *             bounding boxes for each character of the composing text, use
87      *             {@link android.view.inputmethod.CursorAnchorInfo}.
88      */
89     @Deprecated
90     public static final String ACTION_SUGGESTION_PICKED = "android.text.style.SUGGESTION_PICKED";
91 
92     /**
93      * This is deprecated in {@link android.os.Build.VERSION_CODES#Q}.
94      *
95      * @deprecated See {@link #ACTION_SUGGESTION_PICKED}.
96      */
97     @Deprecated
98     public static final String SUGGESTION_SPAN_PICKED_AFTER = "after";
99     /**
100      * This is deprecated in {@link android.os.Build.VERSION_CODES#Q}.
101      *
102      * @deprecated See {@link #ACTION_SUGGESTION_PICKED}.
103      */
104     @Deprecated
105     public static final String SUGGESTION_SPAN_PICKED_BEFORE = "before";
106     /**
107      * This is deprecated in {@link android.os.Build.VERSION_CODES#Q}.
108      *
109      * @deprecated See {@link #ACTION_SUGGESTION_PICKED}.
110      */
111     @Deprecated
112     public static final String SUGGESTION_SPAN_PICKED_HASHCODE = "hashcode";
113 
114     public static final int SUGGESTIONS_MAX_SIZE = 5;
115 
116     /*
117      * TODO: Needs to check the validity and add a feature that TextView will change
118      * the current IME to the other IME which is specified in SuggestionSpan.
119      * An IME needs to set the span by specifying the target IME and Subtype of SuggestionSpan.
120      * And the current IME might want to specify any IME as the target IME including other IMEs.
121      */
122 
123     private int mFlags;
124     private final String[] mSuggestions;
125     /**
126      * Kept for compatibility for apps that rely on invalid locale strings e.g.
127      * {@code new Locale(" an ", " i n v a l i d ", "data")}, which cannot be handled by
128      * {@link #mLanguageTag}.
129      */
130     @NonNull
131     private final String mLocaleStringForCompatibility;
132     @NonNull
133     private final String mLanguageTag;
134     private final int mHashCode;
135 
136     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
137     private float mEasyCorrectUnderlineThickness;
138     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
139     private int mEasyCorrectUnderlineColor;
140 
141     private float mMisspelledUnderlineThickness;
142     private int mMisspelledUnderlineColor;
143 
144     private float mAutoCorrectionUnderlineThickness;
145     private int mAutoCorrectionUnderlineColor;
146 
147     private float mGrammarErrorUnderlineThickness;
148     private int mGrammarErrorUnderlineColor;
149 
150     /**
151      * @param context Context for the application
152      * @param suggestions Suggestions for the string under the span
153      * @param flags Additional flags indicating how this span is handled in TextView
154      */
SuggestionSpan(Context context, String[] suggestions, int flags)155     public SuggestionSpan(Context context, String[] suggestions, int flags) {
156         this(context, null, suggestions, flags, null);
157     }
158 
159     /**
160      * @param locale Locale of the suggestions
161      * @param suggestions Suggestions for the string under the span
162      * @param flags Additional flags indicating how this span is handled in TextView
163      */
SuggestionSpan(Locale locale, String[] suggestions, int flags)164     public SuggestionSpan(Locale locale, String[] suggestions, int flags) {
165         this(null, locale, suggestions, flags, null);
166     }
167 
168     /**
169      * @param context Context for the application
170      * @param locale locale Locale of the suggestions
171      * @param suggestions Suggestions for the string under the span. Only the first up to
172      * {@link SuggestionSpan#SUGGESTIONS_MAX_SIZE} will be considered. Null values not permitted.
173      * @param flags Additional flags indicating how this span is handled in TextView
174      * @param notificationTargetClass if not null, this class will get notified when the user
175      *                                selects one of the suggestions.  On Android
176      *                                {@link android.os.Build.VERSION_CODES#Q} and later this
177      *                                parameter is always ignored.
178      */
SuggestionSpan(Context context, Locale locale, String[] suggestions, int flags, Class<?> notificationTargetClass)179     public SuggestionSpan(Context context, Locale locale, String[] suggestions, int flags,
180             Class<?> notificationTargetClass) {
181         final int N = Math.min(SUGGESTIONS_MAX_SIZE, suggestions.length);
182         mSuggestions = Arrays.copyOf(suggestions, N);
183         mFlags = flags;
184         final Locale sourceLocale;
185         if (locale != null) {
186             sourceLocale = locale;
187         } else if (context != null) {
188             // TODO: Consider to context.getResources().getResolvedLocale() instead.
189             sourceLocale = context.getResources().getConfiguration().locale;
190         } else {
191             Log.e("SuggestionSpan", "No locale or context specified in SuggestionSpan constructor");
192             sourceLocale = null;
193         }
194         mLocaleStringForCompatibility = sourceLocale == null ? "" : sourceLocale.toString();
195         mLanguageTag = sourceLocale == null ? "" : sourceLocale.toLanguageTag();
196         mHashCode = hashCodeInternal(mSuggestions, mLanguageTag, mLocaleStringForCompatibility);
197 
198         initStyle(context);
199     }
200 
initStyle(Context context)201     private void initStyle(Context context) {
202         if (context == null) {
203             mMisspelledUnderlineThickness = 0;
204             mGrammarErrorUnderlineThickness = 0;
205             mEasyCorrectUnderlineThickness = 0;
206             mAutoCorrectionUnderlineThickness = 0;
207             mMisspelledUnderlineColor = Color.BLACK;
208             mGrammarErrorUnderlineColor = Color.BLACK;
209             mEasyCorrectUnderlineColor = Color.BLACK;
210             mAutoCorrectionUnderlineColor = Color.BLACK;
211             return;
212         }
213 
214         int defStyleAttr = com.android.internal.R.attr.textAppearanceMisspelledSuggestion;
215         TypedArray typedArray = context.obtainStyledAttributes(
216                 null, com.android.internal.R.styleable.SuggestionSpan, defStyleAttr, 0);
217         mMisspelledUnderlineThickness = typedArray.getDimension(
218                 com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0);
219         mMisspelledUnderlineColor = typedArray.getColor(
220                 com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK);
221         typedArray.recycle();
222 
223         defStyleAttr = com.android.internal.R.attr.textAppearanceGrammarErrorSuggestion;
224         typedArray = context.obtainStyledAttributes(
225                 null, com.android.internal.R.styleable.SuggestionSpan, defStyleAttr, 0);
226         mGrammarErrorUnderlineThickness = typedArray.getDimension(
227                 com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0);
228         mGrammarErrorUnderlineColor = typedArray.getColor(
229                 com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK);
230         typedArray.recycle();
231 
232         defStyleAttr = com.android.internal.R.attr.textAppearanceEasyCorrectSuggestion;
233         typedArray = context.obtainStyledAttributes(
234                 null, com.android.internal.R.styleable.SuggestionSpan, defStyleAttr, 0);
235         mEasyCorrectUnderlineThickness = typedArray.getDimension(
236                 com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0);
237         mEasyCorrectUnderlineColor = typedArray.getColor(
238                 com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK);
239         typedArray.recycle();
240 
241         defStyleAttr = com.android.internal.R.attr.textAppearanceAutoCorrectionSuggestion;
242         typedArray = context.obtainStyledAttributes(
243                 null, com.android.internal.R.styleable.SuggestionSpan, defStyleAttr, 0);
244         mAutoCorrectionUnderlineThickness = typedArray.getDimension(
245                 com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0);
246         mAutoCorrectionUnderlineColor = typedArray.getColor(
247                 com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK);
248         typedArray.recycle();
249     }
250 
SuggestionSpan(Parcel src)251     public SuggestionSpan(Parcel src) {
252         this(/* suggestions= */ src.readStringArray(), /* flags= */ src.readInt(),
253                 /* localStringForCompatibility= */ src.readString(),
254                 /* languageTag= */ src.readString(), /* hashCode= */ src.readInt(),
255                 /* easyCorrectUnderlineColor= */ src.readInt(),
256                 /* easyCorrectUnderlineThickness= */ src.readFloat(),
257                 /* misspelledUnderlineColor= */ src.readInt(),
258                 /* misspelledUnderlineThickness= */ src.readFloat(),
259                 /* autoCorrectionUnderlineColor= */ src.readInt(),
260                 /* autoCorrectionUnderlineThickness= */ src.readFloat(),
261                 /* grammarErrorUnderlineColor= */ src.readInt(),
262                 /* grammarErrorUnderlineThickness= */ src.readFloat());
263     }
264 
265     /** @hide */
SuggestionSpan(String[] suggestions, int flags, String localeStringForCompatibility, String languageTag, int hashCode, int easyCorrectUnderlineColor, float easyCorrectUnderlineThickness, int misspelledUnderlineColor, float misspelledUnderlineThickness, int autoCorrectionUnderlineColor, float autoCorrectionUnderlineThickness, int grammarErrorUnderlineColor, float grammarErrorUnderlineThickness)266     public SuggestionSpan(String[] suggestions, int flags, String localeStringForCompatibility,
267             String languageTag, int hashCode, int easyCorrectUnderlineColor,
268             float easyCorrectUnderlineThickness, int misspelledUnderlineColor,
269             float misspelledUnderlineThickness, int autoCorrectionUnderlineColor,
270             float autoCorrectionUnderlineThickness, int grammarErrorUnderlineColor,
271             float grammarErrorUnderlineThickness) {
272         mSuggestions = suggestions;
273         mFlags = flags;
274         mLocaleStringForCompatibility = localeStringForCompatibility;
275         mLanguageTag = languageTag;
276         mHashCode = hashCode;
277         mEasyCorrectUnderlineColor = easyCorrectUnderlineColor;
278         mEasyCorrectUnderlineThickness = easyCorrectUnderlineThickness;
279         mMisspelledUnderlineColor = misspelledUnderlineColor;
280         mMisspelledUnderlineThickness = misspelledUnderlineThickness;
281         mAutoCorrectionUnderlineColor = autoCorrectionUnderlineColor;
282         mAutoCorrectionUnderlineThickness = autoCorrectionUnderlineThickness;
283         mGrammarErrorUnderlineColor = grammarErrorUnderlineColor;
284         mGrammarErrorUnderlineThickness = grammarErrorUnderlineThickness;
285     }
286 
287 
288     /**
289      * @return an array of suggestion texts for this span
290      */
getSuggestions()291     public String[] getSuggestions() {
292         return mSuggestions;
293     }
294 
295     /**
296      * @deprecated use {@link #getLocaleObject()} instead.
297      * @return the locale of the suggestions. An empty string is returned if no locale is specified.
298      */
299     @NonNull
300     @Deprecated
getLocale()301     public String getLocale() {
302         return mLocaleStringForCompatibility;
303     }
304 
305     /**
306      * Returns a well-formed BCP 47 language tag representation of the suggestions, as a
307      * {@link Locale} object.
308      *
309      * <p><b>Caveat</b>: The returned object is guaranteed to be a  a well-formed BCP 47 language tag
310      * representation.  For example, this method can return an empty locale rather than returning a
311      * malformed data when this object is initialized with an malformed {@link Locale} object, e.g.
312      * {@code new Locale(" a ", " b c d ", " "}.</p>
313      *
314      * @return the locale of the suggestions. {@code null} is returned if no locale is specified.
315      */
316     @Nullable
getLocaleObject()317     public Locale getLocaleObject() {
318         return mLanguageTag.isEmpty() ? null : Locale.forLanguageTag(mLanguageTag);
319     }
320 
321     /**
322      * @return {@code null}.
323      *
324      * @hide
325      * @deprecated Do not use. Always returns {@code null}.
326      */
327     @Deprecated
328     @UnsupportedAppUsage
getNotificationTargetClassName()329     public String getNotificationTargetClassName() {
330         return null;
331     }
332 
getFlags()333     public int getFlags() {
334         return mFlags;
335     }
336 
setFlags(int flags)337     public void setFlags(int flags) {
338         mFlags = flags;
339     }
340 
341     @Override
describeContents()342     public int describeContents() {
343         return 0;
344     }
345 
346     @Override
writeToParcel(Parcel dest, int flags)347     public void writeToParcel(Parcel dest, int flags) {
348         writeToParcelInternal(dest, flags);
349     }
350 
351     /** @hide */
writeToParcelInternal(Parcel dest, int flags)352     public void writeToParcelInternal(Parcel dest, int flags) {
353         dest.writeStringArray(mSuggestions);
354         dest.writeInt(mFlags);
355         dest.writeString(mLocaleStringForCompatibility);
356         dest.writeString(mLanguageTag);
357         dest.writeInt(mHashCode);
358         dest.writeInt(mEasyCorrectUnderlineColor);
359         dest.writeFloat(mEasyCorrectUnderlineThickness);
360         dest.writeInt(mMisspelledUnderlineColor);
361         dest.writeFloat(mMisspelledUnderlineThickness);
362         dest.writeInt(mAutoCorrectionUnderlineColor);
363         dest.writeFloat(mAutoCorrectionUnderlineThickness);
364         dest.writeInt(mGrammarErrorUnderlineColor);
365         dest.writeFloat(mGrammarErrorUnderlineThickness);
366     }
367 
368     @Override
getSpanTypeId()369     public int getSpanTypeId() {
370         return getSpanTypeIdInternal();
371     }
372 
373     /** @hide */
getSpanTypeIdInternal()374     public int getSpanTypeIdInternal() {
375         return TextUtils.SUGGESTION_SPAN;
376     }
377 
378     @Override
equals(@ullable Object o)379     public boolean equals(@Nullable Object o) {
380         if (o instanceof SuggestionSpan) {
381             return ((SuggestionSpan)o).hashCode() == mHashCode;
382         }
383         return false;
384     }
385 
386     @Override
hashCode()387     public int hashCode() {
388         return mHashCode;
389     }
390 
hashCodeInternal(String[] suggestions, @NonNull String languageTag, @NonNull String localeStringForCompatibility)391     private static int hashCodeInternal(String[] suggestions, @NonNull String languageTag,
392             @NonNull String localeStringForCompatibility) {
393         return Arrays.hashCode(new Object[] {Long.valueOf(SystemClock.uptimeMillis()), suggestions,
394                 languageTag, localeStringForCompatibility});
395     }
396 
397     public static final @android.annotation.NonNull Parcelable.Creator<SuggestionSpan> CREATOR =
398             new Parcelable.Creator<SuggestionSpan>() {
399         @Override
400         public SuggestionSpan createFromParcel(Parcel source) {
401             return new SuggestionSpan(source);
402         }
403 
404         @Override
405         public SuggestionSpan[] newArray(int size) {
406             return new SuggestionSpan[size];
407         }
408     };
409 
410     @Override
updateDrawState(TextPaint tp)411     public void updateDrawState(TextPaint tp) {
412         final boolean misspelled = (mFlags & FLAG_MISSPELLED) != 0;
413         final boolean easy = (mFlags & FLAG_EASY_CORRECT) != 0;
414         final boolean autoCorrection = (mFlags & FLAG_AUTO_CORRECTION) != 0;
415         final boolean grammarError = (mFlags & FLAG_GRAMMAR_ERROR) != 0;
416         if (easy) {
417             if (!misspelled && !grammarError) {
418                 tp.setUnderlineText(mEasyCorrectUnderlineColor, mEasyCorrectUnderlineThickness);
419             } else if (tp.underlineColor == 0) {
420                 // Spans are rendered in an arbitrary order. Since misspelled is less prioritary
421                 // than just easy, do not apply misspelled if an easy (or a mispelled) has been set
422                 if (grammarError) {
423                     tp.setUnderlineText(
424                             mGrammarErrorUnderlineColor, mGrammarErrorUnderlineThickness);
425                 } else {
426                     tp.setUnderlineText(mMisspelledUnderlineColor, mMisspelledUnderlineThickness);
427                 }
428             }
429         } else if (autoCorrection) {
430             tp.setUnderlineText(mAutoCorrectionUnderlineColor, mAutoCorrectionUnderlineThickness);
431         } else if (misspelled) {
432             tp.setUnderlineText(mMisspelledUnderlineColor, mMisspelledUnderlineThickness);
433         } else if (grammarError) {
434             tp.setUnderlineText(mGrammarErrorUnderlineColor, mGrammarErrorUnderlineThickness);
435         }
436     }
437 
438     /**
439      * @return The color of the underline for that span, or 0 if there is no underline
440      */
441     @ColorInt
getUnderlineColor()442     public int getUnderlineColor() {
443         // The order here should match what is used in updateDrawState
444         final boolean misspelled = (mFlags & FLAG_MISSPELLED) != 0;
445         final boolean easy = (mFlags & FLAG_EASY_CORRECT) != 0;
446         final boolean autoCorrection = (mFlags & FLAG_AUTO_CORRECTION) != 0;
447         final boolean grammarError = (mFlags & FLAG_GRAMMAR_ERROR) != 0;
448         if (easy) {
449             if (grammarError) {
450                 return mGrammarErrorUnderlineColor;
451             } else if (misspelled) {
452                 return mMisspelledUnderlineColor;
453             } else {
454                 return mEasyCorrectUnderlineColor;
455             }
456         } else if (autoCorrection) {
457             return mAutoCorrectionUnderlineColor;
458         } else if (misspelled) {
459             return mMisspelledUnderlineColor;
460         } else if (grammarError) {
461             return mGrammarErrorUnderlineColor;
462         }
463         return 0;
464     }
465 
466     /**
467      * Does nothing.
468      *
469      * @deprecated this is deprecated in {@link android.os.Build.VERSION_CODES#Q}.
470      * @hide
471      */
472     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
473     @Deprecated
notifySelection(Context context, String original, int index)474     public void notifySelection(Context context, String original, int index) {
475         Log.w(TAG, "notifySelection() is deprecated.  Does nothing.");
476     }
477 
478     /** @hide */
getEasyCorrectUnderlineThickness()479     public float getEasyCorrectUnderlineThickness() {
480         return mEasyCorrectUnderlineThickness;
481     }
482 
483     /** @hide */
getEasyCorrectUnderlineColor()484     public int getEasyCorrectUnderlineColor() {
485         return mEasyCorrectUnderlineColor;
486     }
487 
488     /** @hide */
getMisspelledUnderlineThickness()489     public float getMisspelledUnderlineThickness() {
490         return mMisspelledUnderlineThickness;
491     }
492 
493     /** @hide */
getMisspelledUnderlineColor()494     public int getMisspelledUnderlineColor() {
495         return mMisspelledUnderlineColor;
496     }
497 
498     /** @hide */
getAutoCorrectionUnderlineThickness()499     public float getAutoCorrectionUnderlineThickness() {
500         return mAutoCorrectionUnderlineThickness;
501     }
502 
503     /** @hide */
getAutoCorrectionUnderlineColor()504     public int getAutoCorrectionUnderlineColor() {
505         return mAutoCorrectionUnderlineColor;
506     }
507 
508     /** @hide */
getGrammarErrorUnderlineThickness()509     public float getGrammarErrorUnderlineThickness() {
510         return mGrammarErrorUnderlineThickness;
511     }
512 
513     /** @hide */
getGrammarErrorUnderlineColor()514     public int getGrammarErrorUnderlineColor() {
515         return mGrammarErrorUnderlineColor;
516     }
517 }
518