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